2020-01-18 09:38:21 +01:00
/*
* Copyright ( c ) 2018 - 2020 , Andreas Kling < kling @ serenityos . org >
2021-07-06 16:44:44 +02:00
* Copyright ( c ) 2021 , Jakob - Niklas See < git @ nwex . de >
2022-03-29 16:33:46 +03:00
* Copyright ( c ) 2022 , the SerenityOS developers .
2020-01-18 09:38:21 +01:00
*
2021-04-22 01:24:48 -07:00
* SPDX - License - Identifier : BSD - 2 - Clause
2020-01-18 09:38:21 +01:00
*/
2021-06-01 21:18:08 +02:00
# include <AK/CharacterTypes.h>
2021-01-24 15:28:26 +01:00
# include <AK/Debug.h>
2020-12-30 13:55:06 +03:30
# include <AK/ScopeGuard.h>
2019-06-07 11:46:02 +02:00
# include <AK/StringBuilder.h>
2020-10-15 20:46:52 +02:00
# include <AK/TemporaryChange.h>
2022-02-03 20:43:07 +00:00
# include <LibCore/File.h>
2020-04-20 21:31:49 +02:00
# include <LibCore/Timer.h>
2020-02-06 20:33:02 +01:00
# include <LibGUI/Action.h>
2020-12-30 13:55:06 +03:30
# include <LibGUI/AutocompleteProvider.h>
2020-02-06 20:33:02 +01:00
# include <LibGUI/Clipboard.h>
2021-01-02 11:59:55 +01:00
# include <LibGUI/EditingEngine.h>
2022-09-08 18:06:08 -04:00
# include <LibGUI/EmojiInputDialog.h>
2020-02-06 20:33:02 +01:00
# include <LibGUI/InputBox.h>
# include <LibGUI/Menu.h>
# include <LibGUI/Painter.h>
2021-01-02 11:59:55 +01:00
# include <LibGUI/RegularEditingEngine.h>
2021-04-13 16:18:20 +02:00
# include <LibGUI/Scrollbar.h>
2020-02-06 20:33:02 +01:00
# include <LibGUI/TextEditor.h>
# include <LibGUI/Window.h>
2020-02-14 23:02:47 +01:00
# include <LibGfx/Bitmap.h>
2022-04-09 09:28:38 +02:00
# include <LibGfx/Font/Font.h>
# include <LibGfx/Font/FontDatabase.h>
2020-02-07 20:07:15 +01:00
# include <LibGfx/Palette.h>
2022-08-18 15:38:06 +02:00
# include <LibGfx/StandardCursor.h>
2021-02-07 15:15:10 +01:00
# include <LibSyntax/Highlighter.h>
2019-03-07 17:06:11 +01:00
# include <fcntl.h>
# include <stdio.h>
2019-06-07 11:46:02 +02:00
# include <unistd.h>
2019-03-07 00:31:06 +01:00
2021-01-02 16:30:13 -07:00
REGISTER_WIDGET ( GUI , TextEditor )
2020-02-02 15:07:41 +01:00
namespace GUI {
2020-02-23 12:07:13 +01:00
TextEditor : : TextEditor ( Type type )
: m_type ( type )
2019-03-07 00:31:06 +01:00
{
2020-09-25 20:43:35 +02:00
REGISTER_STRING_PROPERTY ( " text " , text , set_text ) ;
2021-03-08 11:38:43 -05:00
REGISTER_STRING_PROPERTY ( " placeholder " , placeholder , set_placeholder ) ;
2022-02-02 19:18:23 -05:00
REGISTER_BOOL_PROPERTY ( " gutter " , is_gutter_visible , set_gutter_visible ) ;
REGISTER_BOOL_PROPERTY ( " ruler " , is_ruler_visible , set_ruler_visible ) ;
2020-12-28 13:01:41 +01:00
REGISTER_ENUM_PROPERTY ( " mode " , mode , set_mode , Mode ,
{ Editable , " Editable " } ,
{ ReadOnly , " ReadOnly " } ,
{ DisplayOnly , " DisplayOnly " } ) ;
2020-09-25 20:43:35 +02:00
2020-10-30 10:58:27 +01:00
set_focus_policy ( GUI : : FocusPolicy : : StrongFocus ) ;
2020-05-17 21:51:58 +02:00
set_accepts_emoji_input ( true ) ;
2020-09-11 14:25:48 +02:00
set_override_cursor ( Gfx : : StandardCursor : : IBeam ) ;
2019-12-24 22:01:32 +01:00
set_background_role ( ColorRole : : Base ) ;
set_foreground_role ( ColorRole : : BaseText ) ;
2020-02-02 15:07:41 +01:00
set_document ( TextDocument : : create ( ) ) ;
2020-09-01 23:57:30 +02:00
if ( is_single_line ( ) )
set_visualize_trailing_whitespace ( false ) ;
2019-03-16 16:54:51 +01:00
set_scrollbars_enabled ( is_multi_line ( ) ) ;
2022-02-26 15:22:30 -05:00
if ( is_multi_line ( ) ) {
2020-12-29 18:25:13 +01:00
set_font ( Gfx : : FontDatabase : : default_fixed_width_font ( ) ) ;
2022-02-26 15:22:30 -05:00
set_wrapping_mode ( WrappingMode : : WrapAtWords ) ;
}
2019-06-07 10:42:43 +02:00
vertical_scrollbar ( ) . set_step ( line_height ( ) ) ;
2019-03-19 01:41:00 +01:00
m_cursor = { 0 , 0 } ;
2020-04-20 21:31:49 +02:00
m_automatic_selection_scroll_timer = add < Core : : Timer > ( 100 , [ this ] {
automatic_selection_scroll_timer_fired ( ) ;
} ) ;
m_automatic_selection_scroll_timer - > stop ( ) ;
2019-04-18 12:25:00 +02:00
create_actions ( ) ;
2021-01-02 11:59:55 +01:00
set_editing_engine ( make < RegularEditingEngine > ( ) ) ;
2019-03-07 00:31:06 +01:00
}
2020-02-02 15:07:41 +01:00
TextEditor : : ~ TextEditor ( )
2019-03-07 00:31:06 +01:00
{
2019-10-27 18:00:07 +01:00
if ( m_document )
m_document - > unregister_client ( * this ) ;
2019-03-07 00:31:06 +01:00
}
2020-02-02 15:07:41 +01:00
void TextEditor : : create_actions ( )
2019-04-18 12:25:00 +02:00
{
2020-02-02 15:07:41 +01:00
m_undo_action = CommonActions : : make_undo_action ( [ & ] ( auto & ) { undo ( ) ; } , this ) ;
m_redo_action = CommonActions : : make_redo_action ( [ & ] ( auto & ) { redo ( ) ; } , this ) ;
2019-11-09 01:50:39 -06:00
m_undo_action - > set_enabled ( false ) ;
m_redo_action - > set_enabled ( false ) ;
2020-02-02 15:07:41 +01:00
m_cut_action = CommonActions : : make_cut_action ( [ & ] ( auto & ) { cut ( ) ; } , this ) ;
m_copy_action = CommonActions : : make_copy_action ( [ & ] ( auto & ) { copy ( ) ; } , this ) ;
2021-06-15 17:01:14 +02:00
m_cut_action - > set_enabled ( false ) ;
m_copy_action - > set_enabled ( false ) ;
2020-02-02 15:07:41 +01:00
m_paste_action = CommonActions : : make_paste_action ( [ & ] ( auto & ) { paste ( ) ; } , this ) ;
2022-07-11 17:32:29 +00:00
m_paste_action - > set_enabled ( is_editable ( ) & & Clipboard : : the ( ) . fetch_mime_type ( ) . starts_with ( " text/ " sv ) ) ;
2020-02-10 19:39:30 +01:00
if ( is_multi_line ( ) ) {
m_go_to_line_action = Action : : create (
2022-07-11 17:32:29 +00:00
" Go to line... " , { Mod_Ctrl , Key_L } , Gfx : : Bitmap : : try_load_from_file ( " /res/icons/16x16/go-to.png " sv ) . release_value_but_fixme_should_propagate_errors ( ) , [ this ] ( auto & ) {
2020-07-16 07:54:42 -06:00
String value ;
2022-07-11 17:32:29 +00:00
if ( InputBox : : show ( window ( ) , value , " Line: " sv , " Go to line " sv ) = = InputBox : : ExecResult : : OK ) {
2020-11-10 08:53:50 +00:00
auto line_target = value . to_uint ( ) ;
if ( line_target . has_value ( ) ) {
set_cursor_and_focus_line ( line_target . value ( ) - 1 , 0 ) ;
}
2020-02-10 19:39:30 +01:00
}
} ,
this ) ;
}
2020-07-03 20:54:38 +02:00
m_select_all_action = CommonActions : : make_select_all_action ( [ this ] ( auto & ) { select_all ( ) ; } , this ) ;
2022-09-08 18:06:08 -04:00
m_insert_emoji_action = CommonActions : : make_insert_emoji_action ( [ & ] ( auto & ) { insert_emoji ( ) ; } , this ) ;
2019-04-18 12:25:00 +02:00
}
2021-11-11 00:55:02 +01:00
void TextEditor : : set_text ( StringView text , AllowCallback allow_callback )
2019-03-07 00:31:06 +01:00
{
2019-04-09 16:20:36 +02:00
m_selection . clear ( ) ;
2019-10-27 16:10:07 +01:00
2021-09-21 17:02:48 -04:00
document ( ) . set_text ( text , allow_callback ) ;
2019-10-27 16:10:07 +01:00
2019-03-16 16:54:51 +01:00
update_content_size ( ) ;
2019-08-25 08:43:01 +02:00
recompute_all_visual_lines ( ) ;
2019-03-20 18:12:56 +01:00
if ( is_single_line ( ) )
2019-10-27 16:10:07 +01:00
set_cursor ( 0 , line ( 0 ) . length ( ) ) ;
2019-03-20 18:12:56 +01:00
else
set_cursor ( 0 , 0 ) ;
2019-04-12 02:52:34 +02:00
did_update_selection ( ) ;
2019-03-07 00:31:06 +01:00
update ( ) ;
}
2020-02-02 15:07:41 +01:00
void TextEditor : : update_content_size ( )
2019-03-07 13:54:02 +01:00
{
2019-03-16 16:54:51 +01:00
int content_width = 0 ;
2019-08-28 23:26:24 -05:00
int content_height = 0 ;
2019-10-27 18:00:07 +01:00
for ( auto & line : m_line_visual_data ) {
content_width = max ( line . visual_rect . width ( ) , content_width ) ;
content_height + = line . visual_rect . height ( ) ;
2019-08-28 23:26:24 -05:00
}
2019-03-16 23:16:37 +01:00
content_width + = m_horizontal_content_padding * 2 ;
2019-04-24 23:42:49 +02:00
if ( is_right_text_alignment ( m_text_alignment ) )
content_width = max ( frame_inner_rect ( ) . width ( ) , content_width ) ;
2019-08-28 23:26:24 -05:00
2019-03-16 16:54:51 +01:00
set_content_size ( { content_width , content_height } ) ;
2021-06-12 04:50:23 +03:00
set_size_occupied_by_fixed_elements ( { ruler_width ( ) + gutter_width ( ) , 0 } ) ;
2019-03-07 00:31:06 +01:00
}
2021-07-09 21:34:20 +10:00
TextPosition TextEditor : : text_position_at_content_position ( Gfx : : IntPoint const & content_position ) const
2019-03-07 00:31:06 +01:00
{
2021-01-03 00:11:56 +01:00
auto position = content_position ;
2020-06-29 20:34:42 +02:00
if ( is_single_line ( ) & & icon ( ) )
2021-04-12 11:47:09 -07:00
position . translate_by ( - ( icon_size ( ) + icon_padding ( ) ) , 0 ) ;
2020-06-29 20:34:42 +02:00
2019-12-09 17:45:40 +01:00
size_t line_index = 0 ;
2019-08-25 08:43:01 +02:00
2021-02-15 16:59:13 +01:00
if ( position . y ( ) > = 0 ) {
if ( is_wrapping_enabled ( ) ) {
for ( size_t i = 0 ; i < line_count ( ) ; + + i ) {
auto & rect = m_line_visual_data [ i ] . visual_rect ;
if ( position . y ( ) > = rect . top ( ) & & position . y ( ) < = rect . bottom ( ) ) {
line_index = i ;
break ;
}
if ( position . y ( ) > rect . bottom ( ) )
line_index = line_count ( ) - 1 ;
2019-10-27 18:00:07 +01:00
}
2021-02-15 16:59:13 +01:00
} else {
line_index = ( size_t ) ( position . y ( ) / line_height ( ) ) ;
2019-08-25 08:43:01 +02:00
}
2021-02-15 16:59:13 +01:00
line_index = max ( ( size_t ) 0 , min ( line_index , line_count ( ) - 1 ) ) ;
2019-08-25 08:43:01 +02:00
}
2020-05-18 16:38:28 +02:00
size_t column_index = 0 ;
2019-04-24 22:24:16 +02:00
switch ( m_text_alignment ) {
2020-02-06 11:56:38 +01:00
case Gfx : : TextAlignment : : CenterLeft :
2021-07-09 21:34:20 +10:00
for_each_visual_line ( line_index , [ & ] ( Gfx : : IntRect const & rect , auto & view , size_t start_of_line , [[maybe_unused]] bool is_last_visual_line ) {
2020-11-13 15:29:55 +10:00
if ( is_multi_line ( ) & & ! rect . contains_vertically ( position . y ( ) ) & & ! is_last_visual_line )
2020-05-18 17:55:21 +02:00
return IterationDecision : : Continue ;
column_index = start_of_line ;
if ( position . x ( ) < = 0 ) {
// We're outside the text on the left side, put cursor at column 0 on this visual line.
} else {
int glyph_x = 0 ;
size_t i = 0 ;
for ( ; i < view . length ( ) ; + + i ) {
2022-01-21 18:48:10 +01:00
int advance = font ( ) . glyph_or_emoji_width ( view . code_points ( ) [ i ] ) + font ( ) . glyph_spacing ( ) ;
2020-05-18 17:55:21 +02:00
if ( ( glyph_x + ( advance / 2 ) ) > = position . x ( ) )
break ;
glyph_x + = advance ;
2019-08-25 08:43:01 +02:00
}
2020-05-18 17:55:21 +02:00
column_index + = i ;
2020-05-18 16:38:28 +02:00
}
2020-05-18 17:55:21 +02:00
return IterationDecision : : Break ;
2020-05-18 16:38:28 +02:00
} ) ;
2019-04-24 22:24:16 +02:00
break ;
2020-02-06 11:56:38 +01:00
case Gfx : : TextAlignment : : CenterRight :
2019-08-25 08:43:01 +02:00
// FIXME: Support right-aligned line wrapping, I guess.
2021-02-23 20:42:32 +01:00
VERIFY ( ! is_wrapping_enabled ( ) ) ;
2020-05-18 16:38:28 +02:00
column_index = ( position . x ( ) - content_x_for_position ( { line_index , 0 } ) + fixed_glyph_width ( ) / 2 ) / fixed_glyph_width ( ) ;
2019-04-24 22:24:16 +02:00
break ;
default :
2021-02-23 20:42:32 +01:00
VERIFY_NOT_REACHED ( ) ;
2019-04-24 22:24:16 +02:00
}
2019-12-09 17:45:40 +01:00
column_index = max ( ( size_t ) 0 , min ( column_index , line ( line_index ) . length ( ) ) ) ;
2019-03-07 15:03:38 +01:00
return { line_index , column_index } ;
}
2021-07-09 21:34:20 +10:00
TextPosition TextEditor : : text_position_at ( Gfx : : IntPoint const & widget_position ) const
2021-01-03 00:11:56 +01:00
{
auto content_position = widget_position ;
2021-04-12 11:47:09 -07:00
content_position . translate_by ( horizontal_scrollbar ( ) . value ( ) , vertical_scrollbar ( ) . value ( ) ) ;
2021-06-12 04:50:23 +03:00
content_position . translate_by ( - ( m_horizontal_content_padding + ruler_width ( ) + gutter_width ( ) ) , 0 ) ;
2021-04-12 11:47:09 -07:00
content_position . translate_by ( - frame_thickness ( ) , - frame_thickness ( ) ) ;
2021-01-03 00:11:56 +01:00
return text_position_at_content_position ( content_position ) ;
}
2020-02-02 15:07:41 +01:00
void TextEditor : : doubleclick_event ( MouseEvent & event )
2019-04-25 22:29:25 +02:00
{
2021-10-27 13:20:27 +02:00
if ( event . button ( ) ! = MouseButton : : Primary )
2019-04-25 22:29:25 +02:00
return ;
2020-07-14 17:02:46 -04:00
if ( is_displayonly ( ) )
return ;
2021-04-21 23:48:26 +02:00
if ( ! current_line ( ) . can_select ( ) )
return ;
2021-06-29 11:22:57 +02:00
rehighlight_if_needed ( ) ;
2019-11-08 19:49:08 +01:00
2019-05-16 00:40:55 +02:00
m_triple_click_timer . start ( ) ;
2019-04-25 22:29:25 +02:00
m_in_drag_select = false ;
2021-06-25 11:57:34 +02:00
auto position = text_position_at ( event . position ( ) ) ;
2019-04-25 22:29:25 +02:00
2022-05-07 16:03:33 +02:00
if ( m_substitution_code_point . has_value ( ) ) {
2021-06-25 17:48:51 +02:00
// NOTE: If we substitute the code points, we don't want double clicking to only select a single word, since
// whitespace isn't visible anymore.
m_selection = document ( ) . range_for_entire_line ( position . line ( ) ) ;
} else if ( document ( ) . has_spans ( ) ) {
2019-10-27 16:10:07 +01:00
for ( auto & span : document ( ) . spans ( ) ) {
2021-06-25 11:57:34 +02:00
if ( span . range . contains ( position ) ) {
m_selection = span . range ;
break ;
}
2019-10-27 11:10:32 +01:00
}
2021-06-25 11:57:34 +02:00
} else {
m_selection . set_start ( document ( ) . first_word_break_before ( position , false ) ) ;
m_selection . set_end ( document ( ) . first_word_break_after ( position ) ) ;
2019-04-25 22:29:25 +02:00
}
2021-06-25 11:57:34 +02:00
set_cursor ( m_selection . end ( ) ) ;
2019-04-25 22:29:25 +02:00
update ( ) ;
did_update_selection ( ) ;
}
2020-02-02 15:07:41 +01:00
void TextEditor : : mousedown_event ( MouseEvent & event )
2019-03-07 15:03:38 +01:00
{
2021-10-27 13:20:27 +02:00
if ( event . button ( ) ! = MouseButton : : Primary ) {
2019-05-16 00:15:58 +02:00
return ;
}
2019-03-08 17:53:02 +01:00
2020-07-14 17:02:46 -04:00
if ( on_mousedown )
on_mousedown ( ) ;
if ( is_displayonly ( ) )
return ;
2019-05-16 00:40:55 +02:00
if ( m_triple_click_timer . is_valid ( ) & & m_triple_click_timer . elapsed ( ) < 250 ) {
2020-02-02 12:34:39 +01:00
m_triple_click_timer = Core : : ElapsedTimer ( ) ;
2021-08-15 15:48:53 +10:00
select_current_line ( ) ;
2019-05-16 00:40:55 +02:00
return ;
}
2019-05-16 00:15:58 +02:00
if ( event . modifiers ( ) & Mod_Shift ) {
if ( ! has_selection ( ) )
2019-06-07 11:46:02 +02:00
m_selection . set ( m_cursor , { } ) ;
2019-05-16 00:15:58 +02:00
} else {
m_selection . clear ( ) ;
}
2019-03-08 17:53:02 +01:00
2019-05-16 00:15:58 +02:00
m_in_drag_select = true ;
2020-04-20 21:31:49 +02:00
m_automatic_selection_scroll_timer - > start ( ) ;
2019-03-08 17:53:02 +01:00
2019-05-16 00:15:58 +02:00
set_cursor ( text_position_at ( event . position ( ) ) ) ;
2019-03-08 18:28:24 +01:00
2019-05-16 00:15:58 +02:00
if ( ! ( event . modifiers ( ) & Mod_Shift ) ) {
if ( ! has_selection ( ) )
2019-06-07 11:46:02 +02:00
m_selection . set ( m_cursor , { } ) ;
2019-03-08 17:53:02 +01:00
}
2019-05-16 00:15:58 +02:00
if ( m_selection . start ( ) . is_valid ( ) & & m_selection . start ( ) ! = m_cursor )
m_selection . set_end ( m_cursor ) ;
// FIXME: Only update the relevant rects.
update ( ) ;
did_update_selection ( ) ;
2019-03-08 17:53:02 +01:00
}
2020-02-02 15:07:41 +01:00
void TextEditor : : mouseup_event ( MouseEvent & event )
2019-03-08 17:53:02 +01:00
{
2021-10-27 13:20:27 +02:00
if ( event . button ( ) = = MouseButton : : Primary ) {
2019-03-08 17:53:02 +01:00
if ( m_in_drag_select ) {
m_in_drag_select = false ;
}
return ;
}
}
2020-02-02 15:07:41 +01:00
void TextEditor : : mousemove_event ( MouseEvent & event )
2019-03-08 17:53:02 +01:00
{
2020-04-20 21:31:49 +02:00
m_last_mousemove_position = event . position ( ) ;
2020-04-24 19:14:22 +02:00
if ( m_in_drag_select & & ( rect ( ) . contains ( event . position ( ) ) | | ! m_automatic_selection_scroll_timer - > is_active ( ) ) ) {
2019-03-08 17:53:02 +01:00
set_cursor ( text_position_at ( event . position ( ) ) ) ;
2019-03-08 18:28:24 +01:00
m_selection . set_end ( m_cursor ) ;
2019-04-12 02:52:34 +02:00
did_update_selection ( ) ;
2019-03-08 00:49:45 +01:00
update ( ) ;
2019-03-08 17:53:02 +01:00
return ;
2019-03-08 00:49:45 +01:00
}
2022-08-18 15:38:06 +02:00
if ( m_ruler_visible & & ( ruler_rect_in_inner_coordinates ( ) . contains ( event . position ( ) ) ) ) {
set_override_cursor ( Gfx : : StandardCursor : : None ) ;
} else {
set_editing_cursor ( ) ;
}
2019-03-07 00:31:06 +01:00
}
2021-08-15 15:48:53 +10:00
void TextEditor : : select_current_line ( )
{
m_selection = document ( ) . range_for_entire_line ( m_cursor . line ( ) ) ;
set_cursor ( m_selection . end ( ) ) ;
update ( ) ;
did_update_selection ( ) ;
}
2020-04-20 21:31:49 +02:00
void TextEditor : : automatic_selection_scroll_timer_fired ( )
{
if ( ! m_in_drag_select ) {
m_automatic_selection_scroll_timer - > stop ( ) ;
return ;
}
set_cursor ( text_position_at ( m_last_mousemove_position ) ) ;
m_selection . set_end ( m_cursor ) ;
did_update_selection ( ) ;
update ( ) ;
}
2020-02-02 15:07:41 +01:00
int TextEditor : : ruler_width ( ) const
2019-03-07 20:05:05 +01:00
{
2019-03-15 17:54:05 +01:00
if ( ! m_ruler_visible )
return 0 ;
2020-08-15 10:27:54 -04:00
int line_count_digits = static_cast < int > ( log10 ( line_count ( ) ) ) + 1 ;
2022-02-24 08:48:17 -05:00
auto padding = 5 + ( font ( ) . is_fixed_width ( ) ? 1 : ( line_count_digits - ( line_count ( ) < 10 ? - 1 : 0 ) ) ) ;
auto widest_numeral = font ( ) . bold_variant ( ) . glyph_width ( ' 4 ' ) ;
return line_count ( ) < 10 ? ( line_count_digits + 1 ) * widest_numeral + padding : line_count_digits * widest_numeral + padding ;
2019-03-07 20:05:05 +01:00
}
2021-06-12 04:50:23 +03:00
int TextEditor : : gutter_width ( ) const
{
if ( ! m_gutter_visible )
return 0 ;
return line_height ( ) ; // square gutter
}
2020-06-10 10:57:59 +02:00
Gfx : : IntRect TextEditor : : ruler_content_rect ( size_t line_index ) const
2019-03-07 20:05:05 +01:00
{
2019-03-15 17:54:05 +01:00
if ( ! m_ruler_visible )
2019-06-07 11:46:02 +02:00
return { } ;
2019-03-07 20:05:05 +01:00
return {
2019-03-16 23:16:37 +01:00
0 - ruler_width ( ) + horizontal_scrollbar ( ) . value ( ) ,
2019-08-25 07:14:02 +02:00
line_content_rect ( line_index ) . y ( ) ,
2019-03-07 20:05:05 +01:00
ruler_width ( ) ,
2019-08-25 07:14:02 +02:00
line_content_rect ( line_index ) . height ( )
} ;
}
2021-06-12 04:50:23 +03:00
Gfx : : IntRect TextEditor : : gutter_content_rect ( size_t line_index ) const
{
if ( ! m_gutter_visible )
return { } ;
return {
0 - ruler_width ( ) - gutter_width ( ) + horizontal_scrollbar ( ) . value ( ) ,
line_content_rect ( line_index ) . y ( ) ,
gutter_width ( ) ,
line_content_rect ( line_index ) . height ( )
} ;
}
2020-06-10 10:57:59 +02:00
Gfx : : IntRect TextEditor : : ruler_rect_in_inner_coordinates ( ) const
2019-08-25 07:14:02 +02:00
{
2022-02-02 19:20:09 -05:00
return { gutter_width ( ) , 0 , ruler_width ( ) , widget_inner_rect ( ) . height ( ) } ;
2021-06-12 04:50:23 +03:00
}
Gfx : : IntRect TextEditor : : gutter_rect_in_inner_coordinates ( ) const
{
2022-02-02 19:20:09 -05:00
return { 0 , 0 , gutter_width ( ) , widget_inner_rect ( ) . height ( ) } ;
2019-08-25 07:14:02 +02:00
}
2020-06-10 10:57:59 +02:00
Gfx : : IntRect TextEditor : : visible_text_rect_in_inner_coordinates ( ) const
2019-08-25 07:14:02 +02:00
{
return {
2019-09-01 20:04:25 +02:00
m_horizontal_content_padding + ( m_ruler_visible ? ( ruler_rect_in_inner_coordinates ( ) . right ( ) + 1 ) : 0 ) ,
2019-08-25 07:14:02 +02:00
0 ,
2019-09-01 20:04:25 +02:00
frame_inner_rect ( ) . width ( ) - ( m_horizontal_content_padding * 2 ) - width_occupied_by_vertical_scrollbar ( ) - ruler_width ( ) ,
frame_inner_rect ( ) . height ( ) - height_occupied_by_horizontal_scrollbar ( )
2019-03-07 20:05:05 +01:00
} ;
}
2020-02-02 15:07:41 +01:00
void TextEditor : : paint_event ( PaintEvent & event )
2019-03-07 00:31:06 +01:00
{
2020-04-28 13:15:53 +02:00
Color widget_background_color = palette ( ) . color ( is_enabled ( ) ? background_role ( ) : Gfx : : ColorRole : : Window ) ;
2021-06-29 11:22:57 +02:00
rehighlight_if_needed ( ) ;
2019-11-08 19:49:08 +01:00
2020-02-02 15:07:41 +01:00
Frame : : paint_event ( event ) ;
2019-03-28 16:14:26 +01:00
2020-02-02 15:07:41 +01:00
Painter painter ( * this ) ;
2019-03-29 15:01:54 +01:00
painter . add_clip_rect ( widget_inner_rect ( ) ) ;
painter . add_clip_rect ( event . rect ( ) ) ;
2019-12-24 20:57:54 +01:00
painter . fill_rect ( event . rect ( ) , widget_background_color ) ;
2019-03-07 20:05:05 +01:00
2021-06-25 17:48:51 +02:00
// NOTE: This lambda and TextEditor::text_width_for_font() are used to substitute all glyphs with m_substitution_code_point if necessary.
// Painter::draw_text() and Gfx::Font::width() should not be called directly, but using this lambda and TextEditor::text_width_for_font().
2022-02-02 19:32:28 -05:00
auto draw_text = [ & ] ( Gfx : : IntRect const & rect , auto const & raw_text , Gfx : : Font const & font , Gfx : : TextAlignment alignment , Gfx : : TextAttributes attributes , bool substitute = true ) {
2022-05-07 16:03:33 +02:00
if ( m_substitution_code_point . has_value ( ) & & substitute ) {
2022-01-20 19:59:44 +01:00
painter . draw_text ( rect , substitution_code_point_view ( raw_text . length ( ) ) , font , alignment , attributes . color ) ;
2021-06-25 17:48:51 +02:00
} else {
2022-01-20 19:59:44 +01:00
painter . draw_text ( rect , raw_text , font , alignment , attributes . color ) ;
}
if ( attributes . underline ) {
if ( attributes . underline_style = = Gfx : : TextAttributes : : UnderlineStyle : : Solid )
painter . draw_line ( rect . bottom_left ( ) . translated ( 0 , 1 ) , rect . bottom_right ( ) . translated ( 0 , 1 ) , attributes . underline_color . value_or ( attributes . color ) ) ;
if ( attributes . underline_style = = Gfx : : TextAttributes : : UnderlineStyle : : Wavy )
painter . draw_triangle_wave ( rect . bottom_left ( ) . translated ( 0 , 1 ) , rect . bottom_right ( ) . translated ( 0 , 1 ) , attributes . underline_color . value_or ( attributes . color ) , 2 ) ;
2021-06-25 17:48:51 +02:00
}
} ;
2021-03-15 20:11:43 -04:00
if ( is_displayonly ( ) & & is_focused ( ) ) {
2020-07-14 17:02:46 -04:00
Gfx : : IntRect display_rect {
widget_inner_rect ( ) . x ( ) + 1 ,
widget_inner_rect ( ) . y ( ) + 1 ,
2020-12-12 23:17:41 +01:00
widget_inner_rect ( ) . width ( ) - 2 ,
2020-07-14 17:02:46 -04:00
widget_inner_rect ( ) . height ( ) - 2
} ;
2022-02-02 19:32:58 -05:00
painter . fill_rect ( display_rect , palette ( ) . selection ( ) ) ;
2020-07-14 17:02:46 -04:00
}
2019-04-25 01:09:44 +02:00
painter . translate ( frame_thickness ( ) , frame_thickness ( ) ) ;
2022-02-07 12:25:25 +01:00
if ( ! is_multi_line ( ) & & m_icon ) {
Gfx : : IntRect icon_rect { icon_padding ( ) , 1 , icon_size ( ) , icon_size ( ) } ;
painter . draw_scaled_bitmap ( icon_rect , * m_icon , m_icon - > rect ( ) ) ;
}
2021-06-12 04:50:23 +03:00
if ( m_gutter_visible ) {
auto gutter_rect = gutter_rect_in_inner_coordinates ( ) ;
painter . fill_rect ( gutter_rect , palette ( ) . gutter ( ) ) ;
if ( ! m_ruler_visible )
painter . draw_line ( gutter_rect . top_right ( ) , gutter_rect . bottom_right ( ) , palette ( ) . gutter_border ( ) ) ;
}
2019-03-15 17:54:05 +01:00
if ( m_ruler_visible ) {
2021-06-12 04:50:23 +03:00
auto ruler_rect = ruler_rect_in_inner_coordinates ( ) ;
2020-02-19 11:11:37 +01:00
painter . fill_rect ( ruler_rect , palette ( ) . ruler ( ) ) ;
painter . draw_line ( ruler_rect . top_right ( ) , ruler_rect . bottom_right ( ) , palette ( ) . ruler_border ( ) ) ;
2019-03-15 17:54:05 +01:00
}
2019-03-07 20:05:05 +01:00
2022-05-08 04:02:53 +03:00
auto horizontal_scrollbar_value = horizontal_scrollbar ( ) . value ( ) ;
painter . translate ( - horizontal_scrollbar_value , - vertical_scrollbar ( ) . value ( ) ) ;
if ( m_icon & & horizontal_scrollbar_value > 0 )
painter . translate ( min ( icon_size ( ) + icon_padding ( ) , horizontal_scrollbar_value ) , 0 ) ;
2021-06-12 04:50:23 +03:00
painter . translate ( gutter_width ( ) , 0 ) ;
painter . translate ( ruler_width ( ) , 0 ) ;
2019-03-07 00:31:06 +01:00
2019-12-09 17:45:40 +01:00
size_t first_visible_line = text_position_at ( event . rect ( ) . top_left ( ) ) . line ( ) ;
size_t last_visible_line = text_position_at ( event . rect ( ) . bottom_right ( ) ) . line ( ) ;
2019-03-07 15:03:38 +01:00
2019-03-08 18:28:24 +01:00
auto selection = normalized_selection ( ) ;
bool has_selection = selection . is_valid ( ) ;
2019-03-08 00:49:45 +01:00
2019-03-15 17:54:05 +01:00
if ( m_ruler_visible ) {
2019-12-09 17:45:40 +01:00
for ( size_t i = first_visible_line ; i < = last_visible_line ; + + i ) {
2019-03-15 17:54:05 +01:00
bool is_current_line = i = = m_cursor . line ( ) ;
auto ruler_line_rect = ruler_content_rect ( i ) ;
2022-04-24 19:03:17 +02:00
// NOTE: Shrink the rectangle to be only on the first visual line.
auto const line_height = font ( ) . preferred_line_height ( ) ;
if ( ruler_line_rect . height ( ) > line_height )
ruler_line_rect . set_height ( line_height ) ;
2021-06-25 17:48:51 +02:00
// NOTE: Use Painter::draw_text() directly here, as we want to always draw the line numbers in clear text.
2019-03-15 17:54:05 +01:00
painter . draw_text (
2022-02-24 08:52:31 -05:00
ruler_line_rect . shrunken ( 2 , 0 ) ,
2019-07-27 21:20:38 +02:00
String : : number ( i + 1 ) ,
2020-12-31 01:49:05 +01:00
is_current_line ? font ( ) . bold_variant ( ) : font ( ) ,
2022-02-24 08:52:31 -05:00
Gfx : : TextAlignment : : CenterRight ,
2020-02-19 11:11:37 +01:00
is_current_line ? palette ( ) . ruler_active_text ( ) : palette ( ) . ruler_inactive_text ( ) ) ;
2019-03-15 17:54:05 +01:00
}
2019-03-07 20:05:05 +01:00
}
2022-02-02 19:20:09 -05:00
auto gutter_ruler_width = gutter_width ( ) + ruler_width ( ) ;
Gfx : : IntRect text_clip_rect { 0 , 0 , widget_inner_rect ( ) . width ( ) - gutter_ruler_width , widget_inner_rect ( ) . height ( ) } ;
2021-04-12 11:47:09 -07:00
text_clip_rect . translate_by ( horizontal_scrollbar ( ) . value ( ) , vertical_scrollbar ( ) . value ( ) ) ;
2019-08-25 07:14:02 +02:00
painter . add_clip_rect ( text_clip_rect ) ;
2019-03-07 20:05:05 +01:00
2021-02-27 17:32:55 +01:00
size_t span_index = 0 ;
if ( document ( ) . has_spans ( ) ) {
for ( ; ; ) {
if ( span_index > = document ( ) . spans ( ) . size ( ) | | document ( ) . spans ( ) [ span_index ] . range . end ( ) . line ( ) > = first_visible_line ) {
break ;
}
+ + span_index ;
}
}
2019-12-09 17:45:40 +01:00
for ( size_t line_index = first_visible_line ; line_index < = last_visible_line ; + + line_index ) {
auto & line = this - > line ( line_index ) ;
2019-08-25 10:23:11 +02:00
bool physical_line_has_selection = has_selection & & line_index > = selection . start ( ) . line ( ) & & line_index < = selection . end ( ) . line ( ) ;
2019-12-09 17:45:40 +01:00
size_t first_visual_line_with_selection = 0 ;
size_t last_visual_line_with_selection = 0 ;
2019-08-25 10:23:11 +02:00
if ( physical_line_has_selection ) {
2019-08-25 14:04:46 +02:00
if ( selection . start ( ) . line ( ) < line_index )
2019-08-25 10:23:11 +02:00
first_visual_line_with_selection = 0 ;
2019-08-25 14:04:46 +02:00
else
2019-10-27 18:00:07 +01:00
first_visual_line_with_selection = visual_line_containing ( line_index , selection . start ( ) . column ( ) ) ;
2019-08-25 14:04:46 +02:00
if ( selection . end ( ) . line ( ) > line_index )
2019-10-27 18:00:07 +01:00
last_visual_line_with_selection = m_line_visual_data [ line_index ] . visual_line_breaks . size ( ) ;
2019-08-25 14:04:46 +02:00
else
2019-10-27 18:00:07 +01:00
last_visual_line_with_selection = visual_line_containing ( line_index , selection . end ( ) . column ( ) ) ;
2019-08-25 10:23:11 +02:00
}
2019-12-09 17:45:40 +01:00
size_t selection_start_column_within_line = selection . start ( ) . line ( ) = = line_index ? selection . start ( ) . column ( ) : 0 ;
size_t selection_end_column_within_line = selection . end ( ) . line ( ) = = line_index ? selection . end ( ) . column ( ) : line . length ( ) ;
2019-08-25 10:23:11 +02:00
2019-12-09 17:45:40 +01:00
size_t visual_line_index = 0 ;
2022-02-10 18:22:44 +01:00
size_t last_start_of_visual_line = 0 ;
Optional < size_t > multiline_trailing_space_offset { } ;
2021-07-09 21:34:20 +10:00
for_each_visual_line ( line_index , [ & ] ( Gfx : : IntRect const & visual_line_rect , auto & visual_line_text , size_t start_of_visual_line , [[maybe_unused]] bool is_last_visual_line ) {
2022-02-10 18:22:44 +01:00
ScopeGuard update_last_start_of_visual_line { [ & ] ( ) { last_start_of_visual_line = start_of_visual_line ; } } ;
2022-02-02 19:28:58 -05:00
if ( is_focused ( ) & & is_multi_line ( ) & & line_index = = m_cursor . line ( ) & & is_cursor_line_highlighted ( ) ) {
Gfx : : IntRect visible_content_line_rect {
visible_content_rect ( ) . x ( ) ,
visual_line_rect . y ( ) ,
widget_inner_rect ( ) . width ( ) - gutter_ruler_width ,
line_height ( )
} ;
painter . fill_rect ( visible_content_line_rect , widget_background_color . darkened ( 0.9f ) ) ;
}
2021-05-01 21:10:08 +02:00
if constexpr ( TEXTEDITOR_DEBUG )
painter . draw_rect ( visual_line_rect , Color : : Cyan ) ;
2020-09-20 12:20:24 -07:00
2021-07-06 16:44:44 +02:00
if ( ! placeholder ( ) . is_empty ( ) & & document ( ) . is_empty ( ) & & line_index = = 0 ) {
2020-09-20 12:20:24 -07:00
auto line_rect = visual_line_rect ;
2021-06-25 17:48:51 +02:00
line_rect . set_width ( text_width_for_font ( placeholder ( ) , font ( ) ) ) ;
2022-01-20 19:59:44 +01:00
draw_text ( line_rect , placeholder ( ) , font ( ) , m_text_alignment , { palette ( ) . color ( Gfx : : ColorRole : : PlaceholderText ) } , false ) ;
2020-09-20 12:20:24 -07:00
} else if ( ! document ( ) . has_spans ( ) ) {
2019-10-25 21:05:06 +02:00
// Fast-path for plain text
2020-03-04 21:19:35 +01:00
auto color = palette ( ) . color ( is_enabled ( ) ? foreground_role ( ) : Gfx : : ColorRole : : DisabledText ) ;
2021-03-15 20:11:43 -04:00
if ( is_displayonly ( ) & & is_focused ( ) )
2020-07-14 17:02:46 -04:00
color = palette ( ) . color ( is_enabled ( ) ? Gfx : : ColorRole : : SelectionText : Gfx : : ColorRole : : DisabledText ) ;
2022-01-20 19:59:44 +01:00
draw_text ( visual_line_rect , visual_line_text , font ( ) , m_text_alignment , { color } ) ;
2019-10-25 21:05:06 +02:00
} else {
2021-02-27 17:32:55 +01:00
auto unspanned_color = palette ( ) . color ( is_enabled ( ) ? foreground_role ( ) : Gfx : : ColorRole : : DisabledText ) ;
2021-03-15 20:11:43 -04:00
if ( is_displayonly ( ) & & is_focused ( ) )
2021-02-27 17:32:55 +01:00
unspanned_color = palette ( ) . color ( is_enabled ( ) ? Gfx : : ColorRole : : SelectionText : Gfx : : ColorRole : : DisabledText ) ;
RefPtr < Gfx : : Font > unspanned_font = this - > font ( ) ;
size_t next_column = 0 ;
Gfx : : IntRect span_rect = { visual_line_rect . location ( ) , { 0 , line_height ( ) } } ;
2022-01-20 19:59:44 +01:00
auto draw_text_helper = [ & ] ( size_t start , size_t end , RefPtr < Gfx : : Font > & font , Gfx : : TextAttributes text_attributes ) {
2021-06-03 23:09:35 +02:00
size_t length = end - start ;
if ( length = = 0 )
return ;
2021-02-27 17:32:55 +01:00
auto text = visual_line_text . substring_view ( start , length ) ;
2021-02-27 21:06:46 +01:00
span_rect . set_width ( font - > width ( text ) ) ;
2022-01-20 19:59:44 +01:00
if ( text_attributes . background_color . has_value ( ) ) {
painter . fill_rect ( span_rect , text_attributes . background_color . value ( ) ) ;
2021-02-27 17:32:55 +01:00
}
2022-01-20 19:59:44 +01:00
draw_text ( span_rect , text , * font , m_text_alignment , text_attributes ) ;
2022-01-23 23:01:23 +01:00
span_rect . translate_by ( span_rect . width ( ) , 0 ) ;
2021-02-27 17:32:55 +01:00
} ;
for ( ; ; ) {
if ( span_index > = document ( ) . spans ( ) . size ( ) ) {
break ;
}
auto & span = document ( ) . spans ( ) [ span_index ] ;
2021-03-02 09:36:52 +03:30
if ( ! span . range . is_valid ( ) ) {
+ + span_index ;
continue ;
}
2021-02-27 17:32:55 +01:00
if ( span . range . end ( ) . line ( ) < line_index ) {
2021-06-03 02:05:58 -07:00
dbgln_if ( TEXTEDITOR_DEBUG , " spans not sorted (span end {}:{} is before current line {}) => ignoring " , span . range . end ( ) . line ( ) , span . range . end ( ) . column ( ) , line_index ) ;
2021-02-27 17:32:55 +01:00
+ + span_index ;
continue ;
}
2021-02-27 21:06:46 +01:00
if ( span . range . start ( ) . line ( ) > line_index
| | ( span . range . start ( ) . line ( ) = = line_index & & span . range . start ( ) . column ( ) > = start_of_visual_line + visual_line_text . length ( ) ) ) {
2021-02-27 17:32:55 +01:00
// no more spans in this line, moving on
break ;
2020-03-12 16:36:25 +02:00
}
2021-02-27 21:06:46 +01:00
if ( span . range . start ( ) . line ( ) = = span . range . end ( ) . line ( ) & & span . range . end ( ) . column ( ) < span . range . start ( ) . column ( ) ) {
2022-01-23 23:04:48 +01:00
dbgln_if ( TEXTEDITOR_DEBUG , " span from {}:{} to {}:{} has negative length => ignoring " , span . range . start ( ) . line ( ) , span . range . start ( ) . column ( ) , span . range . end ( ) . line ( ) , span . range . end ( ) . column ( ) ) ;
2021-02-27 21:06:46 +01:00
+ + span_index ;
continue ;
}
if ( span . range . end ( ) . line ( ) = = line_index & & span . range . end ( ) . column ( ) < start_of_visual_line + next_column ) {
2021-06-03 02:05:58 -07:00
dbgln_if ( TEXTEDITOR_DEBUG , " spans not sorted (span end {}:{} is before current position {}:{}) => ignoring " ,
2021-02-27 21:06:46 +01:00
span . range . end ( ) . line ( ) , span . range . end ( ) . column ( ) , line_index , start_of_visual_line + next_column ) ;
2021-02-27 17:32:55 +01:00
+ + span_index ;
continue ;
}
size_t span_start ;
2021-02-27 21:06:46 +01:00
if ( span . range . start ( ) . line ( ) < line_index | | span . range . start ( ) . column ( ) < start_of_visual_line ) {
2021-02-27 17:32:55 +01:00
span_start = 0 ;
} else {
2021-02-27 21:06:46 +01:00
span_start = span . range . start ( ) . column ( ) - start_of_visual_line ;
2021-02-27 17:32:55 +01:00
}
2021-03-02 09:36:52 +03:30
if ( span_start < next_column ) {
2021-06-03 02:05:58 -07:00
dbgln_if ( TEXTEDITOR_DEBUG , " span started before the current position, maybe two spans overlap? (span start {} is before current position {}) => ignoring " , span_start , next_column ) ;
2021-03-02 09:36:52 +03:30
+ + span_index ;
continue ;
}
2021-02-27 17:32:55 +01:00
size_t span_end ;
2021-02-27 21:06:46 +01:00
bool span_consumned ;
2021-06-03 23:09:35 +02:00
if ( span . range . end ( ) . line ( ) > line_index | | span . range . end ( ) . column ( ) > start_of_visual_line + visual_line_text . length ( ) ) {
span_end = visual_line_text . length ( ) ;
2021-02-27 21:06:46 +01:00
span_consumned = false ;
2021-02-27 17:32:55 +01:00
} else {
2021-02-27 21:06:46 +01:00
span_end = span . range . end ( ) . column ( ) - start_of_visual_line ;
span_consumned = true ;
2021-02-27 17:32:55 +01:00
}
2021-06-03 23:09:35 +02:00
2021-02-27 17:32:55 +01:00
if ( span_start ! = next_column ) {
// draw unspanned text between spans
2022-01-20 19:59:44 +01:00
draw_text_helper ( next_column , span_start , unspanned_font , { unspanned_color } ) ;
2021-02-27 17:32:55 +01:00
}
auto font = unspanned_font ;
if ( span . attributes . bold ) {
2022-01-31 20:18:15 -05:00
if ( auto bold_font = Gfx : : FontDatabase : : the ( ) . get ( font - > family ( ) , font - > presentation_size ( ) , 700 , 0 ) )
2021-02-27 17:32:55 +01:00
font = bold_font ;
}
2022-01-20 19:59:44 +01:00
draw_text_helper ( span_start , span_end , font , span . attributes ) ;
2021-06-03 23:09:35 +02:00
next_column = span_end ;
2021-02-27 21:06:46 +01:00
if ( ! span_consumned ) {
2021-02-27 17:32:55 +01:00
// continue with same span on next line
break ;
} else {
+ + span_index ;
}
}
// draw unspanned text after last span
if ( next_column < visual_line_text . length ( ) ) {
2022-01-20 19:59:44 +01:00
draw_text_helper ( next_column , visual_line_text . length ( ) , unspanned_font , { unspanned_color } ) ;
2019-10-25 21:05:06 +02:00
}
2021-02-27 21:06:46 +01:00
// consume all spans that should end this line
// this is necessary since the spans can include the new line character
while ( is_last_visual_line & & span_index < document ( ) . spans ( ) . size ( ) ) {
auto & span = document ( ) . spans ( ) [ span_index ] ;
if ( span . range . end ( ) . line ( ) = = line_index ) {
+ + span_index ;
} else {
break ;
}
}
2019-10-25 21:05:06 +02:00
}
2020-05-18 16:38:28 +02:00
2020-09-01 19:10:55 +02:00
if ( m_visualize_trailing_whitespace & & line . ends_in_whitespace ( ) ) {
size_t physical_column ;
auto last_non_whitespace_column = line . last_non_whitespace_column ( ) ;
if ( last_non_whitespace_column . has_value ( ) )
physical_column = last_non_whitespace_column . value ( ) + 1 ;
else
physical_column = 0 ;
size_t end_of_visual_line = ( start_of_visual_line + visual_line_text . length ( ) ) ;
if ( physical_column < end_of_visual_line ) {
2022-02-10 18:22:44 +01:00
physical_column - = multiline_trailing_space_offset . value_or ( 0 ) ;
2020-09-01 19:10:55 +02:00
size_t visual_column = physical_column > start_of_visual_line ? ( physical_column - start_of_visual_line ) : 0 ;
2022-02-10 18:22:44 +01:00
2020-09-01 19:10:55 +02:00
Gfx : : IntRect whitespace_rect {
2022-02-10 17:40:16 +01:00
content_x_for_position ( { line_index , physical_column } ) ,
2020-09-01 19:10:55 +02:00
visual_line_rect . y ( ) ,
2021-06-25 17:48:51 +02:00
text_width_for_font ( visual_line_text . substring_view ( visual_column , visual_line_text . length ( ) - visual_column ) , font ( ) ) ,
2020-09-01 19:10:55 +02:00
visual_line_rect . height ( )
} ;
painter . fill_rect_with_dither_pattern ( whitespace_rect , Color ( ) , Color ( 255 , 192 , 192 ) ) ;
2022-02-10 18:22:44 +01:00
if ( ! multiline_trailing_space_offset . has_value ( ) )
multiline_trailing_space_offset = physical_column - last_start_of_visual_line ;
2020-09-01 19:10:55 +02:00
}
}
2021-03-17 13:52:42 -03:00
if ( m_visualize_leading_whitespace & & line . leading_spaces ( ) > 0 ) {
size_t physical_column = line . leading_spaces ( ) ;
2021-12-27 03:31:05 -08:00
if ( start_of_visual_line < physical_column ) {
size_t end_of_leading_whitespace = min ( physical_column - start_of_visual_line , visual_line_text . length ( ) ) ;
2021-03-17 13:52:42 -03:00
Gfx : : IntRect whitespace_rect {
content_x_for_position ( { line_index , start_of_visual_line } ) ,
visual_line_rect . y ( ) ,
2021-06-25 17:48:51 +02:00
text_width_for_font ( visual_line_text . substring_view ( 0 , end_of_leading_whitespace ) , font ( ) ) ,
2021-03-17 13:52:42 -03:00
visual_line_rect . height ( )
} ;
painter . fill_rect_with_dither_pattern ( whitespace_rect , Color ( ) , Color ( 192 , 255 , 192 ) ) ;
}
}
2021-10-21 19:38:59 +02:00
if ( physical_line_has_selection & & window ( ) - > focused_widget ( ) = = this ) {
2022-04-24 18:26:34 +02:00
size_t const start_of_selection_within_visual_line = ( size_t ) max ( 0 , ( int ) selection_start_column_within_line - ( int ) start_of_visual_line ) ;
size_t const end_of_selection_within_visual_line = min ( selection_end_column_within_line - start_of_visual_line , visual_line_text . length ( ) ) ;
2019-08-25 10:23:11 +02:00
2020-04-24 20:01:57 +02:00
bool current_visual_line_has_selection = start_of_selection_within_visual_line ! = end_of_selection_within_visual_line
& & ( ( line_index ! = selection . start ( ) . line ( ) & & line_index ! = selection . end ( ) . line ( ) )
| | ( visual_line_index > = first_visual_line_with_selection & & visual_line_index < = last_visual_line_with_selection ) ) ;
2019-08-25 10:23:11 +02:00
if ( current_visual_line_has_selection ) {
bool selection_begins_on_current_visual_line = visual_line_index = = first_visual_line_with_selection ;
bool selection_ends_on_current_visual_line = visual_line_index = = last_visual_line_with_selection ;
int selection_left = selection_begins_on_current_visual_line
2019-12-17 21:15:10 +01:00
? content_x_for_position ( { line_index , ( size_t ) selection_start_column_within_line } )
2019-08-25 10:23:11 +02:00
: m_horizontal_content_padding ;
int selection_right = selection_ends_on_current_visual_line
2019-12-17 21:15:10 +01:00
? content_x_for_position ( { line_index , ( size_t ) selection_end_column_within_line } )
2019-09-01 17:30:23 +02:00
: visual_line_rect . right ( ) + 1 ;
2019-08-25 10:23:11 +02:00
2020-06-10 10:57:59 +02:00
Gfx : : IntRect selection_rect {
2019-08-25 10:23:11 +02:00
selection_left ,
visual_line_rect . y ( ) ,
selection_right - selection_left ,
visual_line_rect . height ( )
} ;
2021-10-21 19:38:59 +02:00
Color background_color = window ( ) - > is_active ( ) ? palette ( ) . selection ( ) : palette ( ) . inactive_selection ( ) ;
Color text_color = window ( ) - > is_active ( ) ? palette ( ) . selection_text ( ) : palette ( ) . inactive_selection_text ( ) ;
2020-02-15 16:26:52 +01:00
painter . fill_rect ( selection_rect , background_color ) ;
2019-08-25 10:23:11 +02:00
2020-08-05 16:31:20 -04:00
if ( visual_line_text . code_points ( ) ) {
2020-05-29 14:10:36 -04:00
Utf32View visual_selected_text {
2020-08-05 16:31:20 -04:00
visual_line_text . code_points ( ) + start_of_selection_within_visual_line ,
2020-05-29 14:10:36 -04:00
end_of_selection_within_visual_line - start_of_selection_within_visual_line
} ;
2019-08-25 10:23:11 +02:00
2022-01-20 19:59:44 +01:00
draw_text ( selection_rect , visual_selected_text , font ( ) , Gfx : : TextAlignment : : CenterLeft , { text_color } ) ;
2020-05-29 14:10:36 -04:00
}
2019-08-25 10:23:11 +02:00
}
2019-08-25 08:43:01 +02:00
}
2020-09-01 19:10:55 +02:00
2019-08-25 10:23:11 +02:00
+ + visual_line_index ;
2019-08-25 08:43:01 +02:00
return IterationDecision : : Continue ;
} ) ;
2019-03-07 00:31:06 +01:00
}
2022-01-09 21:44:44 +01:00
if ( is_enabled ( ) & & is_focused ( ) & & m_cursor_state & & ! is_displayonly ( ) )
2020-02-20 09:01:48 +01:00
painter . fill_rect ( cursor_content_rect ( ) , palette ( ) . text_cursor ( ) ) ;
2019-03-07 00:31:06 +01:00
}
2022-02-22 20:25:30 +01:00
Optional < UISize > TextEditor : : calculated_min_size ( ) const
{
auto margins = content_margins ( ) ;
int horizontal = margins . left ( ) + margins . right ( ) ,
vertical = margins . top ( ) + margins . bottom ( ) ;
int vertical_content_size = font ( ) . glyph_height ( ) + 4 ;
if ( ! is_multi_line ( ) & & m_icon )
vertical_content_size = max ( vertical_content_size , icon_size ( ) + 2 ) ;
return UISize ( horizontal , vertical ) ;
}
2020-02-02 15:07:41 +01:00
void TextEditor : : select_all ( )
2019-06-22 10:38:38 +02:00
{
2020-02-02 15:07:41 +01:00
TextPosition start_of_document { 0 , 0 } ;
TextPosition end_of_document { line_count ( ) - 1 , line ( line_count ( ) - 1 ) . length ( ) } ;
2020-05-27 18:41:54 +02:00
m_selection . set ( end_of_document , start_of_document ) ;
2019-06-22 10:38:38 +02:00
did_update_selection ( ) ;
2020-05-27 18:41:54 +02:00
set_cursor ( start_of_document ) ;
2019-06-22 10:38:38 +02:00
update ( ) ;
}
2022-09-08 18:06:08 -04:00
void TextEditor : : insert_emoji ( )
{
2022-09-09 07:21:31 -04:00
if ( ! accepts_emoji_input ( ) | | window ( ) - > blocks_emoji_input ( ) )
return ;
2022-09-08 18:06:08 -04:00
auto emoji_input_dialog = EmojiInputDialog : : construct ( window ( ) ) ;
emoji_input_dialog - > set_window_mode ( GUI : : WindowMode : : Passive ) ;
if ( emoji_input_dialog - > exec ( ) ! = EmojiInputDialog : : ExecResult : : OK )
return ;
auto emoji_code_point = emoji_input_dialog - > selected_emoji_text ( ) ;
insert_at_cursor_or_replace_selection ( emoji_code_point ) ;
}
2020-02-02 15:07:41 +01:00
void TextEditor : : keydown_event ( KeyEvent & event )
2019-03-07 00:31:06 +01:00
{
2022-07-31 16:51:50 +02:00
if ( ! is_editable ( ) & & event . key ( ) = = KeyCode : : Key_Tab )
return AbstractScrollableWidget : : keydown_event ( event ) ;
2020-12-30 13:55:06 +03:30
if ( m_autocomplete_box & & m_autocomplete_box - > is_visible ( ) & & ( event . key ( ) = = KeyCode : : Key_Return | | event . key ( ) = = KeyCode : : Key_Tab ) ) {
2021-10-28 20:31:54 -05:00
TemporaryChange change { m_should_keep_autocomplete_box , true } ;
2022-05-14 17:09:24 +03:00
if ( m_autocomplete_box - > apply_suggestion ( ) = = CodeComprehension : : AutocompleteResultEntry : : HideAutocompleteAfterApplying : : Yes )
2021-10-28 20:31:54 -05:00
hide_autocomplete ( ) ;
else
try_update_autocomplete ( ) ;
2020-12-30 13:55:06 +03:30
return ;
}
if ( m_autocomplete_box & & m_autocomplete_box - > is_visible ( ) & & event . key ( ) = = KeyCode : : Key_Escape ) {
2021-10-28 20:23:58 -05:00
hide_autocomplete ( ) ;
2020-12-30 13:55:06 +03:30
return ;
}
if ( m_autocomplete_box & & m_autocomplete_box - > is_visible ( ) & & event . key ( ) = = KeyCode : : Key_Up ) {
m_autocomplete_box - > previous_suggestion ( ) ;
return ;
}
if ( m_autocomplete_box & & m_autocomplete_box - > is_visible ( ) & & event . key ( ) = = KeyCode : : Key_Down ) {
m_autocomplete_box - > next_suggestion ( ) ;
return ;
}
2021-01-02 11:59:55 +01:00
if ( is_single_line ( ) ) {
if ( event . key ( ) = = KeyCode : : Key_Tab )
2021-05-03 20:31:58 +02:00
return AbstractScrollableWidget : : keydown_event ( event ) ;
2019-06-23 07:53:58 +02:00
2021-06-02 13:21:17 +02:00
if ( event . modifiers ( ) = = KeyModifier : : Mod_Shift & & event . key ( ) = = KeyCode : : Key_Return ) {
if ( on_shift_return_pressed )
on_shift_return_pressed ( ) ;
return ;
}
2022-03-19 12:09:44 +01:00
if ( event . modifiers ( ) = = KeyModifier : : Mod_Ctrl & & event . key ( ) = = KeyCode : : Key_Return ) {
if ( on_ctrl_return_pressed )
on_ctrl_return_pressed ( ) ;
return ;
}
2021-06-02 13:21:17 +02:00
2021-01-02 11:59:55 +01:00
if ( event . key ( ) = = KeyCode : : Key_Return ) {
if ( on_return_pressed )
on_return_pressed ( ) ;
return ;
2020-12-30 13:55:06 +03:30
}
2021-01-02 11:59:55 +01:00
if ( event . key ( ) = = KeyCode : : Key_Up ) {
if ( on_up_pressed )
on_up_pressed ( ) ;
return ;
2019-03-07 17:11:17 +01:00
}
2021-01-02 11:59:55 +01:00
if ( event . key ( ) = = KeyCode : : Key_Down ) {
if ( on_down_pressed )
on_down_pressed ( ) ;
return ;
2019-03-07 17:11:17 +01:00
}
2021-01-02 11:59:55 +01:00
if ( event . key ( ) = = KeyCode : : Key_PageUp ) {
if ( on_pageup_pressed )
on_pageup_pressed ( ) ;
return ;
2020-10-08 14:00:40 -04:00
}
2021-01-02 11:59:55 +01:00
if ( event . key ( ) = = KeyCode : : Key_PageDown ) {
if ( on_pagedown_pressed )
on_pagedown_pressed ( ) ;
2019-11-15 20:17:49 +01:00
return ;
}
2021-01-02 11:59:55 +01:00
2021-04-07 09:34:31 +04:30
} else if ( ! is_multi_line ( ) ) {
VERIFY_NOT_REACHED ( ) ;
}
2021-01-02 11:59:55 +01:00
2021-04-07 09:34:31 +04:30
ArmedScopeGuard update_autocomplete { [ & ] {
2021-10-28 02:04:33 -05:00
try_update_autocomplete ( ) ;
2021-04-07 09:34:31 +04:30
} } ;
if ( is_multi_line ( ) & & ! event . shift ( ) & & ! event . alt ( ) & & event . ctrl ( ) & & event . key ( ) = = KeyCode : : Key_Space ) {
if ( m_autocomplete_provider ) {
2021-10-27 23:13:49 -05:00
try_show_autocomplete ( UserRequestedAutocomplete : : Yes ) ;
2021-04-07 09:34:31 +04:30
update_autocomplete . disarm ( ) ;
return ;
2020-10-08 14:00:40 -04:00
}
2019-03-07 13:21:51 +01:00
}
2021-01-02 11:59:55 +01:00
if ( m_editing_engine - > on_key ( event ) )
2019-03-07 13:21:51 +01:00
return ;
2021-01-02 11:59:55 +01:00
if ( event . key ( ) = = KeyCode : : Key_Escape ) {
if ( on_escape_pressed )
on_escape_pressed ( ) ;
2019-03-07 13:21:51 +01:00
return ;
}
2021-01-02 11:59:55 +01:00
if ( event . modifiers ( ) = = Mod_Shift & & event . key ( ) = = KeyCode : : Key_Delete ) {
if ( m_autocomplete_box )
2021-10-28 20:23:58 -05:00
hide_autocomplete ( ) ;
2019-03-07 13:21:51 +01:00
return ;
}
2021-01-02 11:59:55 +01:00
2022-06-18 12:02:51 -07:00
if ( event . key ( ) = = KeyCode : : Key_Tab ) {
if ( has_selection ( ) ) {
2022-06-18 12:03:49 -07:00
if ( event . modifiers ( ) = = Mod_Shift ) {
unindent_selection ( ) ;
return ;
}
2022-06-18 12:02:51 -07:00
if ( is_indenting_selection ( ) ) {
indent_selection ( ) ;
return ;
}
}
}
2021-01-02 11:59:55 +01:00
if ( event . key ( ) = = KeyCode : : Key_Delete ) {
2021-08-20 18:56:43 +01:00
if ( ! is_editable ( ) )
return ;
2021-01-02 11:59:55 +01:00
if ( m_autocomplete_box )
2021-10-28 20:23:58 -05:00
hide_autocomplete ( ) ;
2021-08-20 18:56:43 +01:00
if ( has_selection ( ) ) {
delete_selection ( ) ;
did_update_selection ( ) ;
return ;
}
if ( m_cursor . column ( ) < current_line ( ) . length ( ) ) {
// Delete within line
2021-08-20 19:08:34 +01:00
int erase_count = 1 ;
if ( event . modifiers ( ) = = Mod_Ctrl ) {
auto word_break_pos = document ( ) . first_word_break_after ( m_cursor ) ;
erase_count = word_break_pos . column ( ) - m_cursor . column ( ) ;
}
TextRange erased_range ( m_cursor , { m_cursor . line ( ) , m_cursor . column ( ) + erase_count } ) ;
2021-08-20 18:56:43 +01:00
execute < RemoveTextCommand > ( document ( ) . text_in_range ( erased_range ) , erased_range ) ;
return ;
}
if ( m_cursor . column ( ) = = current_line ( ) . length ( ) & & m_cursor . line ( ) ! = line_count ( ) - 1 ) {
// Delete at end of line; merge with next line
2021-08-20 19:08:34 +01:00
size_t erase_count = 0 ;
if ( event . modifiers ( ) = = Mod_Ctrl ) {
erase_count = document ( ) . first_word_break_after ( { m_cursor . line ( ) + 1 , 0 } ) . column ( ) ;
}
TextRange erased_range ( m_cursor , { m_cursor . line ( ) + 1 , erase_count } ) ;
2021-08-20 18:56:43 +01:00
execute < RemoveTextCommand > ( document ( ) . text_in_range ( erased_range ) , erased_range ) ;
return ;
}
2019-11-15 13:45:27 -06:00
return ;
}
2021-01-02 11:59:55 +01:00
2019-03-08 18:50:14 +01:00
if ( event . key ( ) = = KeyCode : : Key_Backspace ) {
2020-07-15 17:04:49 -04:00
if ( ! is_editable ( ) )
2019-05-08 05:00:28 +02:00
return ;
2021-01-01 17:29:11 +03:30
if ( m_autocomplete_box )
2021-10-28 20:23:58 -05:00
hide_autocomplete ( ) ;
2019-03-08 14:10:34 +01:00
if ( has_selection ( ) ) {
delete_selection ( ) ;
2019-04-12 02:52:34 +02:00
did_update_selection ( ) ;
2019-03-08 14:10:34 +01:00
return ;
}
2019-03-07 16:04:21 +01:00
if ( m_cursor . column ( ) > 0 ) {
2019-10-27 10:42:48 +01:00
int erase_count = 1 ;
2020-07-21 22:28:59 -07:00
if ( event . modifiers ( ) = = Mod_Ctrl ) {
auto word_break_pos = document ( ) . first_word_break_before ( m_cursor , true ) ;
erase_count = m_cursor . column ( ) - word_break_pos . column ( ) ;
} else if ( current_line ( ) . first_non_whitespace_column ( ) > = m_cursor . column ( ) ) {
2019-10-27 10:42:48 +01:00
int new_column ;
if ( m_cursor . column ( ) % m_soft_tab_width = = 0 )
new_column = m_cursor . column ( ) - m_soft_tab_width ;
else
new_column = ( m_cursor . column ( ) / m_soft_tab_width ) * m_soft_tab_width ;
erase_count = m_cursor . column ( ) - new_column ;
}
2019-03-07 16:15:25 +01:00
// Backspace within line
2020-02-02 15:07:41 +01:00
TextRange erased_range ( { m_cursor . line ( ) , m_cursor . column ( ) - erase_count } , m_cursor ) ;
2019-11-30 16:50:24 +01:00
auto erased_text = document ( ) . text_in_range ( erased_range ) ;
execute < RemoveTextCommand > ( erased_text , erased_range ) ;
2019-03-07 17:18:22 +01:00
return ;
2019-03-07 16:04:21 +01:00
}
2019-03-07 16:15:25 +01:00
if ( m_cursor . column ( ) = = 0 & & m_cursor . line ( ) ! = 0 ) {
2019-03-07 16:33:07 +01:00
// Backspace at column 0; merge with previous line
2019-12-09 17:45:40 +01:00
size_t previous_length = line ( m_cursor . line ( ) - 1 ) . length ( ) ;
2020-02-02 15:07:41 +01:00
TextRange erased_range ( { m_cursor . line ( ) - 1 , previous_length } , m_cursor ) ;
2019-11-30 16:50:24 +01:00
execute < RemoveTextCommand > ( " \n " , erased_range ) ;
2019-03-07 17:18:22 +01:00
return ;
2019-03-07 16:15:25 +01:00
}
2019-03-07 16:04:21 +01:00
return ;
}
2022-05-13 19:06:26 +02:00
// AltGr is emulated as Ctrl+Alt; if Ctrl is set check if it's not for AltGr
if ( ( ! event . ctrl ( ) | | event . altgr ( ) ) & & ! event . alt ( ) & & event . code_point ( ) ! = 0 ) {
2021-10-27 23:39:55 -05:00
TemporaryChange change { m_should_keep_autocomplete_box , true } ;
2021-01-02 11:59:55 +01:00
add_code_point ( event . code_point ( ) ) ;
2020-02-24 10:20:25 +01:00
return ;
}
event . ignore ( ) ;
2019-03-07 00:31:06 +01:00
}
2022-06-18 12:02:51 -07:00
bool TextEditor : : is_indenting_selection ( )
{
auto const selection_start = m_selection . start ( ) > m_selection . end ( ) ? m_selection . end ( ) : m_selection . start ( ) ;
auto const selection_end = m_selection . end ( ) > m_selection . start ( ) ? m_selection . end ( ) : m_selection . start ( ) ;
auto const whole_line_selected = selection_end . column ( ) - selection_start . column ( ) > = current_line ( ) . length ( ) - current_line ( ) . first_non_whitespace_column ( ) ;
auto const on_same_line = selection_start . line ( ) = = selection_end . line ( ) ;
if ( has_selection ( ) & & ( whole_line_selected | | ! on_same_line ) ) {
return true ;
}
return false ;
}
void TextEditor : : indent_selection ( )
{
auto const selection_start = m_selection . start ( ) > m_selection . end ( ) ? m_selection . end ( ) : m_selection . start ( ) ;
auto const selection_end = m_selection . end ( ) > m_selection . start ( ) ? m_selection . end ( ) : m_selection . start ( ) ;
if ( is_indenting_selection ( ) ) {
execute < IndentSelection > ( m_soft_tab_width , TextRange ( selection_start , selection_end ) ) ;
m_selection . set_start ( { selection_start . line ( ) , selection_start . column ( ) + m_soft_tab_width } ) ;
m_selection . set_end ( { selection_end . line ( ) , selection_end . column ( ) + m_soft_tab_width } ) ;
set_cursor ( { m_cursor . line ( ) , m_cursor . column ( ) + m_soft_tab_width } ) ;
}
}
2022-06-18 12:03:49 -07:00
void TextEditor : : unindent_selection ( )
{
auto const selection_start = m_selection . start ( ) > m_selection . end ( ) ? m_selection . end ( ) : m_selection . start ( ) ;
auto const selection_end = m_selection . end ( ) > m_selection . start ( ) ? m_selection . end ( ) : m_selection . start ( ) ;
if ( current_line ( ) . first_non_whitespace_column ( ) ! = 0 ) {
if ( current_line ( ) . first_non_whitespace_column ( ) > m_soft_tab_width & & selection_start . column ( ) ! = 0 ) {
m_selection . set_start ( { selection_start . line ( ) , selection_start . column ( ) - m_soft_tab_width } ) ;
m_selection . set_end ( { selection_end . line ( ) , selection_end . column ( ) - m_soft_tab_width } ) ;
} else if ( selection_start . column ( ) ! = 0 ) {
m_selection . set_start ( { selection_start . line ( ) , selection_start . column ( ) - current_line ( ) . leading_spaces ( ) } ) ;
m_selection . set_end ( { selection_end . line ( ) , selection_end . column ( ) - current_line ( ) . leading_spaces ( ) } ) ;
}
execute < UnindentSelection > ( m_soft_tab_width , TextRange ( selection_start , selection_end ) ) ;
}
}
2021-07-13 22:12:07 -05:00
void TextEditor : : delete_previous_word ( )
{
TextRange to_erase ( document ( ) . first_word_before ( m_cursor , true ) , m_cursor ) ;
execute < RemoveTextCommand > ( document ( ) . text_in_range ( to_erase ) , to_erase ) ;
}
2020-02-02 15:07:41 +01:00
void TextEditor : : delete_current_line ( )
2019-03-25 13:13:46 +01:00
{
if ( has_selection ( ) )
return delete_selection ( ) ;
2020-02-02 15:07:41 +01:00
TextPosition start ;
TextPosition end ;
2019-12-09 17:45:40 +01:00
if ( m_cursor . line ( ) = = 0 & & line_count ( ) = = 1 ) {
2019-11-30 17:19:35 +01:00
start = { 0 , 0 } ;
end = { 0 , line ( 0 ) . length ( ) } ;
2019-12-09 17:45:40 +01:00
} else if ( m_cursor . line ( ) = = line_count ( ) - 1 ) {
2021-01-25 23:19:07 +10:00
start = { m_cursor . line ( ) - 1 , line ( m_cursor . line ( ) - 1 ) . length ( ) } ;
2019-11-30 17:19:35 +01:00
end = { m_cursor . line ( ) , line ( m_cursor . line ( ) ) . length ( ) } ;
} else {
start = { m_cursor . line ( ) , 0 } ;
end = { m_cursor . line ( ) + 1 , 0 } ;
}
2019-03-25 13:13:46 +01:00
2020-02-02 15:07:41 +01:00
TextRange erased_range ( start , end ) ;
2019-11-30 17:19:35 +01:00
execute < RemoveTextCommand > ( document ( ) . text_in_range ( erased_range ) , erased_range ) ;
2019-03-25 13:13:46 +01:00
}
2021-07-15 15:50:03 -05:00
void TextEditor : : delete_previous_char ( )
{
if ( ! is_editable ( ) )
return ;
if ( has_selection ( ) )
return delete_selection ( ) ;
TextRange to_erase ( { m_cursor . line ( ) , m_cursor . column ( ) - 1 } , m_cursor ) ;
if ( m_cursor . column ( ) = = 0 & & m_cursor . line ( ) ! = 0 ) {
size_t prev_line_len = line ( m_cursor . line ( ) - 1 ) . length ( ) ;
to_erase . set_start ( { m_cursor . line ( ) - 1 , prev_line_len } ) ;
}
execute < RemoveTextCommand > ( document ( ) . text_in_range ( to_erase ) , to_erase ) ;
}
2021-07-15 16:35:45 -05:00
void TextEditor : : delete_from_line_start_to_cursor ( )
{
TextPosition start ( m_cursor . line ( ) , current_line ( ) . first_non_whitespace_column ( ) ) ;
TextRange to_erase ( start , m_cursor ) ;
execute < RemoveTextCommand > ( document ( ) . text_in_range ( to_erase ) , to_erase ) ;
}
2020-02-02 15:07:41 +01:00
void TextEditor : : do_delete ( )
2019-03-20 23:11:00 +01:00
{
2020-07-15 17:04:49 -04:00
if ( ! is_editable ( ) )
2019-05-08 05:00:28 +02:00
return ;
2019-03-25 13:13:46 +01:00
if ( has_selection ( ) )
return delete_selection ( ) ;
2019-03-20 23:11:00 +01:00
if ( m_cursor . column ( ) < current_line ( ) . length ( ) ) {
// Delete within line
2020-02-02 15:07:41 +01:00
TextRange erased_range ( m_cursor , { m_cursor . line ( ) , m_cursor . column ( ) + 1 } ) ;
2019-11-30 16:58:07 +01:00
execute < RemoveTextCommand > ( document ( ) . text_in_range ( erased_range ) , erased_range ) ;
2019-03-20 23:11:00 +01:00
return ;
}
if ( m_cursor . column ( ) = = current_line ( ) . length ( ) & & m_cursor . line ( ) ! = line_count ( ) - 1 ) {
// Delete at end of line; merge with next line
2020-02-02 15:07:41 +01:00
TextRange erased_range ( m_cursor , { m_cursor . line ( ) + 1 , 0 } ) ;
2019-11-30 16:58:07 +01:00
execute < RemoveTextCommand > ( document ( ) . text_in_range ( erased_range ) , erased_range ) ;
2019-03-20 23:11:00 +01:00
return ;
}
}
2021-01-02 11:59:55 +01:00
void TextEditor : : add_code_point ( u32 code_point )
{
if ( ! is_editable ( ) )
return ;
StringBuilder sb ;
sb . append_code_point ( code_point ) ;
if ( should_autocomplete_automatically ( ) ) {
if ( sb . string_view ( ) . is_whitespace ( ) )
m_autocomplete_timer - > stop ( ) ;
else
m_autocomplete_timer - > start ( ) ;
}
insert_at_cursor_or_replace_selection ( sb . to_string ( ) ) ;
} ;
void TextEditor : : reset_cursor_blink ( )
{
m_cursor_state = true ;
update_cursor ( ) ;
stop_timer ( ) ;
start_timer ( 500 ) ;
}
2021-04-26 20:29:05 +00:00
void TextEditor : : update_selection ( bool is_selecting )
2021-01-02 11:59:55 +01:00
{
2021-10-11 22:51:40 +02:00
if ( is_selecting & & ! selection ( ) . is_valid ( ) ) {
selection ( ) . set ( cursor ( ) , { } ) ;
2021-01-02 11:59:55 +01:00
did_update_selection ( ) ;
update ( ) ;
return ;
}
2021-10-11 22:51:40 +02:00
if ( ! is_selecting & & selection ( ) . is_valid ( ) ) {
selection ( ) . clear ( ) ;
2021-01-02 11:59:55 +01:00
did_update_selection ( ) ;
update ( ) ;
return ;
}
2021-10-11 22:51:40 +02:00
if ( is_selecting & & selection ( ) . start ( ) . is_valid ( ) ) {
selection ( ) . set_end ( cursor ( ) ) ;
2021-01-02 11:59:55 +01:00
did_update_selection ( ) ;
update ( ) ;
return ;
}
}
2021-07-09 21:34:20 +10:00
int TextEditor : : content_x_for_position ( TextPosition const & position ) const
2019-04-24 22:24:16 +02:00
{
2019-12-09 17:45:40 +01:00
auto & line = this - > line ( position . line ( ) ) ;
2020-05-18 16:38:28 +02:00
int x_offset = 0 ;
2019-04-24 22:24:16 +02:00
switch ( m_text_alignment ) {
2020-02-06 11:56:38 +01:00
case Gfx : : TextAlignment : : CenterLeft :
2021-07-09 21:34:20 +10:00
for_each_visual_line ( position . line ( ) , [ & ] ( Gfx : : IntRect const & , auto & visual_line_view , size_t start_of_visual_line , bool is_last_visual_line ) {
2020-05-18 16:38:28 +02:00
size_t offset_in_visual_line = position . column ( ) - start_of_visual_line ;
2021-02-23 12:34:26 +11:00
auto before_line_end = is_last_visual_line ? ( offset_in_visual_line < = visual_line_view . length ( ) ) : ( offset_in_visual_line < visual_line_view . length ( ) ) ;
if ( position . column ( ) > = start_of_visual_line & & before_line_end ) {
2020-05-18 16:38:28 +02:00
if ( offset_in_visual_line = = 0 ) {
x_offset = 0 ;
} else {
2021-06-25 17:48:51 +02:00
x_offset = text_width_for_font ( visual_line_view . substring_view ( 0 , offset_in_visual_line ) , font ( ) ) ;
2020-05-18 16:38:28 +02:00
x_offset + = font ( ) . glyph_spacing ( ) ;
}
2019-08-25 09:06:54 +02:00
return IterationDecision : : Break ;
}
return IterationDecision : : Continue ;
} ) ;
2020-06-29 20:34:42 +02:00
return m_horizontal_content_padding + ( ( is_single_line ( ) & & icon ( ) ) ? ( icon_size ( ) + icon_padding ( ) ) : 0 ) + x_offset ;
2020-02-06 11:56:38 +01:00
case Gfx : : TextAlignment : : CenterRight :
2019-08-25 09:06:54 +02:00
// FIXME
2021-02-23 20:42:32 +01:00
VERIFY ( ! is_wrapping_enabled ( ) ) ;
2020-05-18 16:38:28 +02:00
return content_width ( ) - m_horizontal_content_padding - ( line . length ( ) * fixed_glyph_width ( ) ) + ( position . column ( ) * fixed_glyph_width ( ) ) ;
2019-04-24 22:24:16 +02:00
default :
2021-02-23 20:42:32 +01:00
VERIFY_NOT_REACHED ( ) ;
2019-04-24 22:24:16 +02:00
}
}
2021-06-25 17:48:51 +02:00
int TextEditor : : text_width_for_font ( auto const & text , Gfx : : Font const & font ) const
{
2022-05-07 16:03:33 +02:00
if ( m_substitution_code_point . has_value ( ) )
2021-06-25 17:48:51 +02:00
return font . width ( substitution_code_point_view ( text . length ( ) ) ) ;
else
return font . width ( text ) ;
}
Utf32View TextEditor : : substitution_code_point_view ( size_t length ) const
{
2022-05-07 16:03:33 +02:00
VERIFY ( m_substitution_code_point . has_value ( ) ) ;
2021-06-25 17:48:51 +02:00
if ( ! m_substitution_string_data )
m_substitution_string_data = make < Vector < u32 > > ( ) ;
if ( ! m_substitution_string_data - > is_empty ( ) )
VERIFY ( m_substitution_string_data - > first ( ) = = m_substitution_code_point ) ;
while ( m_substitution_string_data - > size ( ) < length )
2022-05-07 16:03:33 +02:00
m_substitution_string_data - > append ( m_substitution_code_point . value ( ) ) ;
2021-06-25 17:48:51 +02:00
return Utf32View { m_substitution_string_data - > data ( ) , length } ;
}
2021-07-09 21:34:20 +10:00
Gfx : : IntRect TextEditor : : content_rect_for_position ( TextPosition const & position ) const
2019-03-07 00:31:06 +01:00
{
2019-08-21 21:23:17 +02:00
if ( ! position . is_valid ( ) )
2019-06-07 11:46:02 +02:00
return { } ;
2021-02-23 20:42:32 +01:00
VERIFY ( ! lines ( ) . is_empty ( ) ) ;
VERIFY ( position . column ( ) < = ( current_line ( ) . length ( ) + 1 ) ) ;
2019-04-24 22:24:16 +02:00
2019-08-21 21:23:17 +02:00
int x = content_x_for_position ( position ) ;
2019-04-24 22:24:16 +02:00
2019-03-28 15:30:29 +01:00
if ( is_single_line ( ) ) {
2021-01-02 00:33:14 +01:00
Gfx : : IntRect rect { x , 0 , 1 , line_height ( ) } ;
2019-08-21 21:23:17 +02:00
rect . center_vertically_within ( { { } , frame_inner_rect ( ) . size ( ) } ) ;
return rect ;
2019-03-28 15:30:29 +01:00
}
2019-08-25 09:06:54 +02:00
2020-06-10 10:57:59 +02:00
Gfx : : IntRect rect ;
2021-07-09 21:34:20 +10:00
for_each_visual_line ( position . line ( ) , [ & ] ( Gfx : : IntRect const & visual_line_rect , auto & view , size_t start_of_visual_line , bool is_last_visual_line ) {
2021-02-23 12:34:26 +11:00
auto before_line_end = is_last_visual_line ? ( ( position . column ( ) - start_of_visual_line ) < = view . length ( ) ) : ( ( position . column ( ) - start_of_visual_line ) < view . length ( ) ) ;
if ( position . column ( ) > = start_of_visual_line & & before_line_end ) {
2019-08-25 09:06:54 +02:00
// NOTE: We have to subtract the horizontal padding here since it's part of the visual line rect
// *and* included in what we get from content_x_for_position().
rect = {
visual_line_rect . x ( ) + x - ( m_horizontal_content_padding ) ,
visual_line_rect . y ( ) ,
2021-01-02 11:59:55 +01:00
m_editing_engine - > cursor_width ( ) = = CursorWidth : : WIDE ? 7 : 1 ,
2019-08-25 09:06:54 +02:00
line_height ( )
} ;
return IterationDecision : : Break ;
}
return IterationDecision : : Continue ;
} ) ;
return rect ;
2019-08-21 21:23:17 +02:00
}
2020-06-10 10:57:59 +02:00
Gfx : : IntRect TextEditor : : cursor_content_rect ( ) const
2019-08-21 21:23:17 +02:00
{
return content_rect_for_position ( m_cursor ) ;
2019-03-07 01:05:35 +01:00
}
2020-06-10 10:57:59 +02:00
Gfx : : IntRect TextEditor : : line_widget_rect ( size_t line_index ) const
2019-03-07 02:24:46 +01:00
{
auto rect = line_content_rect ( line_index ) ;
2019-04-24 23:15:15 +02:00
rect . set_x ( frame_thickness ( ) ) ;
rect . set_width ( frame_inner_rect ( ) . width ( ) ) ;
2021-04-12 11:47:09 -07:00
rect . translate_by ( 0 , - ( vertical_scrollbar ( ) . value ( ) ) ) ;
rect . translate_by ( 0 , frame_thickness ( ) ) ;
2019-04-25 01:09:44 +02:00
rect . intersect ( frame_inner_rect ( ) ) ;
2019-03-07 13:13:25 +01:00
return rect ;
2019-03-07 02:24:46 +01:00
}
2021-07-09 21:34:20 +10:00
void TextEditor : : scroll_position_into_view ( TextPosition const & position )
2019-03-07 00:31:06 +01:00
{
2019-08-21 21:23:17 +02:00
auto rect = content_rect_for_position ( position ) ;
if ( position . column ( ) = = 0 )
rect . set_x ( content_x_for_position ( { position . line ( ) , 0 } ) - 2 ) ;
2019-12-09 17:45:40 +01:00
else if ( position . column ( ) = = line ( position . line ( ) ) . length ( ) )
rect . set_x ( content_x_for_position ( { position . line ( ) , line ( position . line ( ) ) . length ( ) } ) + 2 ) ;
2019-04-25 01:33:59 +02:00
scroll_into_view ( rect , true , true ) ;
2019-03-07 00:31:06 +01:00
}
2020-02-02 15:07:41 +01:00
void TextEditor : : scroll_cursor_into_view ( )
2019-08-21 21:23:17 +02:00
{
2020-06-07 21:03:29 +02:00
if ( ! m_reflow_deferred )
scroll_position_into_view ( m_cursor ) ;
2019-08-21 21:23:17 +02:00
}
2020-06-10 10:57:59 +02:00
Gfx : : IntRect TextEditor : : line_content_rect ( size_t line_index ) const
2019-03-07 00:31:06 +01:00
{
2019-12-09 17:45:40 +01:00
auto & line = this - > line ( line_index ) ;
2019-03-28 15:30:29 +01:00
if ( is_single_line ( ) ) {
2021-06-25 17:48:51 +02:00
Gfx : : IntRect line_rect = { content_x_for_position ( { line_index , 0 } ) , 0 , text_width_for_font ( line . view ( ) , font ( ) ) , font ( ) . glyph_height ( ) + 4 } ;
2019-06-07 11:46:02 +02:00
line_rect . center_vertically_within ( { { } , frame_inner_rect ( ) . size ( ) } ) ;
2019-03-28 15:30:29 +01:00
return line_rect ;
}
2021-01-09 22:47:48 +10:00
if ( is_wrapping_enabled ( ) )
2019-10-27 18:00:07 +01:00
return m_line_visual_data [ line_index ] . visual_rect ;
2019-03-07 00:31:06 +01:00
return {
2019-04-24 22:46:27 +02:00
content_x_for_position ( { line_index , 0 } ) ,
2019-12-09 17:45:40 +01:00
( int ) line_index * line_height ( ) ,
2021-06-25 17:48:51 +02:00
text_width_for_font ( line . view ( ) , font ( ) ) ,
2019-03-07 01:05:35 +01:00
line_height ( )
2019-03-07 00:31:06 +01:00
} ;
}
2020-11-10 08:53:50 +00:00
void TextEditor : : set_cursor_and_focus_line ( size_t line , size_t column )
{
u_int index_max = line_count ( ) - 1 ;
set_cursor ( line , column ) ;
if ( line > 1 & & line < index_max ) {
int headroom = frame_inner_rect ( ) . height ( ) / 3 ;
do {
auto line_data = m_line_visual_data [ line ] ;
headroom - = line_data . visual_rect . height ( ) ;
line - - ;
} while ( line > 0 & & headroom > 0 ) ;
Gfx : : IntRect rect = { 0 , line_content_rect ( line ) . y ( ) ,
1 , frame_inner_rect ( ) . height ( ) } ;
scroll_into_view ( rect , false , true ) ;
}
}
2020-02-02 15:07:41 +01:00
void TextEditor : : update_cursor ( )
2019-03-07 00:31:06 +01:00
{
2019-03-07 13:23:17 +01:00
update ( line_widget_rect ( m_cursor . line ( ) ) ) ;
2019-03-07 00:31:06 +01:00
}
2020-02-02 15:07:41 +01:00
void TextEditor : : set_cursor ( size_t line , size_t column )
2019-03-07 00:46:29 +01:00
{
2019-03-07 15:03:38 +01:00
set_cursor ( { line , column } ) ;
}
2021-07-09 21:34:20 +10:00
void TextEditor : : set_cursor ( TextPosition const & a_position )
2019-03-07 15:03:38 +01:00
{
2021-02-23 20:42:32 +01:00
VERIFY ( ! lines ( ) . is_empty ( ) ) ;
2019-10-21 18:58:27 +02:00
2020-02-02 15:07:41 +01:00
TextPosition position = a_position ;
2019-10-21 18:58:27 +02:00
2019-12-09 17:45:40 +01:00
if ( position . line ( ) > = line_count ( ) )
position . set_line ( line_count ( ) - 1 ) ;
2019-10-21 18:58:27 +02:00
2019-10-27 16:10:07 +01:00
if ( position . column ( ) > lines ( ) [ position . line ( ) ] . length ( ) )
position . set_column ( lines ( ) [ position . line ( ) ] . length ( ) ) ;
2019-10-21 18:58:27 +02:00
2020-06-13 00:31:46 +02:00
if ( m_cursor ! = position & & is_visual_data_up_to_date ( ) ) {
2019-05-06 22:04:53 +02:00
// NOTE: If the old cursor is no longer valid, repaint everything just in case.
2019-12-09 17:45:40 +01:00
auto old_cursor_line_rect = m_cursor . line ( ) < line_count ( )
2019-05-06 22:04:53 +02:00
? line_widget_rect ( m_cursor . line ( ) )
: rect ( ) ;
2019-03-08 17:53:02 +01:00
m_cursor = position ;
m_cursor_state = true ;
scroll_cursor_into_view ( ) ;
update ( old_cursor_line_rect ) ;
update_cursor ( ) ;
2020-06-13 00:31:46 +02:00
} else if ( m_cursor ! = position ) {
m_cursor = position ;
m_cursor_state = true ;
2019-03-08 17:53:02 +01:00
}
2019-11-18 19:10:06 +01:00
cursor_did_change ( ) ;
2019-03-07 00:46:29 +01:00
if ( on_cursor_change )
2019-04-10 03:08:29 +02:00
on_cursor_change ( ) ;
2020-02-07 20:07:15 +01:00
if ( m_highlighter )
m_highlighter - > cursor_did_change ( ) ;
2019-03-07 00:46:29 +01:00
}
2020-08-14 19:58:38 +02:00
void TextEditor : : focusin_event ( FocusEvent & event )
2019-03-07 01:05:35 +01:00
{
2020-08-14 19:58:38 +02:00
if ( event . source ( ) = = FocusSource : : Keyboard )
select_all ( ) ;
2020-05-21 17:37:28 +02:00
m_cursor_state = true ;
2019-03-07 01:05:35 +01:00
update_cursor ( ) ;
2021-01-02 11:59:55 +01:00
stop_timer ( ) ;
2019-03-07 01:05:35 +01:00
start_timer ( 500 ) ;
2020-07-04 22:37:55 +04:30
if ( on_focusin )
on_focusin ( ) ;
2019-03-07 01:05:35 +01:00
}
2020-08-14 19:56:40 +02:00
void TextEditor : : focusout_event ( FocusEvent & )
2019-03-07 01:05:35 +01:00
{
2021-03-10 11:57:59 -05:00
if ( is_displayonly ( ) & & has_selection ( ) )
m_selection . clear ( ) ;
2019-03-07 01:05:35 +01:00
stop_timer ( ) ;
2020-07-04 22:37:55 +04:30
if ( on_focusout )
on_focusout ( ) ;
2019-03-07 01:05:35 +01:00
}
2020-02-02 15:07:41 +01:00
void TextEditor : : timer_event ( Core : : TimerEvent & )
2019-03-07 01:05:35 +01:00
{
m_cursor_state = ! m_cursor_state ;
2019-11-05 16:37:47 -06:00
if ( is_focused ( ) )
2019-03-07 01:05:35 +01:00
update_cursor ( ) ;
}
2021-06-30 06:13:06 -07:00
bool TextEditor : : write_to_file ( String const & path )
2019-03-07 17:06:11 +01:00
{
2022-01-16 22:43:40 -05:00
auto file = Core : : File : : construct ( path ) ;
if ( ! file - > open ( Core : : OpenMode : : WriteOnly | Core : : OpenMode : : Truncate ) ) {
warnln ( " Error opening {}: {} " , path , strerror ( file - > error ( ) ) ) ;
2019-03-07 17:06:11 +01:00
return false ;
}
2019-08-28 19:32:45 +02:00
2022-01-16 22:43:40 -05:00
return write_to_file ( * file ) ;
2021-06-30 06:13:06 -07:00
}
2022-01-16 22:43:40 -05:00
bool TextEditor : : write_to_file ( Core : : File & file )
2021-06-30 06:13:06 -07:00
{
2019-08-28 19:32:45 +02:00
off_t file_size = 0 ;
2021-02-15 18:02:33 +01:00
if ( line_count ( ) = = 1 & & line ( 0 ) . is_empty ( ) ) {
// Truncate to zero.
} else {
// Compute the final file size and ftruncate() to make writing fast.
// FIXME: Remove this once the kernel is smart enough to do this instead.
for ( size_t i = 0 ; i < line_count ( ) ; + + i )
file_size + = line ( i ) . length ( ) ;
file_size + = line_count ( ) ;
}
2019-08-28 19:32:45 +02:00
2022-01-16 22:43:40 -05:00
if ( ! file . truncate ( file_size ) ) {
2019-08-28 19:32:45 +02:00
perror ( " ftruncate " ) ;
return false ;
}
2021-06-09 17:07:55 +01:00
if ( file_size = = 0 ) {
// A size 0 file doesn't need a data copy.
} else {
for ( size_t i = 0 ; i < line_count ( ) ; + + i ) {
auto & line = this - > line ( i ) ;
if ( line . length ( ) ) {
auto line_as_utf8 = line . to_utf8 ( ) ;
2022-01-16 22:43:40 -05:00
ssize_t nwritten = file . write ( line_as_utf8 ) ;
2021-06-09 17:07:55 +01:00
if ( nwritten < 0 ) {
perror ( " write " ) ;
return false ;
}
}
char ch = ' \n ' ;
2022-01-16 22:43:40 -05:00
ssize_t nwritten = file . write ( ( u8 * ) & ch , 1 ) ;
2021-06-09 17:07:55 +01:00
if ( nwritten ! = 1 ) {
2019-03-07 17:06:11 +01:00
perror ( " write " ) ;
return false ;
}
}
}
2021-05-16 21:30:46 +02:00
document ( ) . set_unmodified ( ) ;
2019-03-07 17:06:11 +01:00
return true ;
}
2019-03-08 01:59:59 +01:00
2020-02-02 15:07:41 +01:00
String TextEditor : : text ( ) const
2019-03-15 17:37:13 +01:00
{
2020-09-28 16:37:37 +03:00
return document ( ) . text ( ) ;
2019-03-15 17:37:13 +01:00
}
2020-02-02 15:07:41 +01:00
void TextEditor : : clear ( )
2019-03-15 17:37:13 +01:00
{
2019-10-27 19:36:59 +01:00
document ( ) . remove_all_lines ( ) ;
2020-02-02 15:07:41 +01:00
document ( ) . append_line ( make < TextDocumentLine > ( document ( ) ) ) ;
2019-03-15 17:37:13 +01:00
m_selection . clear ( ) ;
2019-04-12 02:52:34 +02:00
did_update_selection ( ) ;
2019-03-15 17:37:13 +01:00
set_cursor ( 0 , 0 ) ;
update ( ) ;
}
2020-02-02 15:07:41 +01:00
String TextEditor : : selected_text ( ) const
2019-03-08 01:59:59 +01:00
{
if ( ! has_selection ( ) )
2019-06-07 11:46:02 +02:00
return { } ;
2019-03-08 01:59:59 +01:00
2019-10-29 21:36:47 +01:00
return document ( ) . text_in_range ( m_selection ) ;
2019-03-08 01:59:59 +01:00
}
2019-03-08 14:08:15 +01:00
2021-05-25 20:58:55 -04:00
size_t TextEditor : : number_of_selected_words ( ) const
{
if ( ! has_selection ( ) )
return 0 ;
size_t word_count = 0 ;
bool in_word = false ;
auto selected_text = this - > selected_text ( ) ;
for ( char c : selected_text ) {
2021-06-01 21:18:08 +02:00
if ( in_word & & is_ascii_space ( c ) ) {
2021-05-25 20:58:55 -04:00
in_word = false ;
word_count + + ;
continue ;
}
2021-06-01 21:18:08 +02:00
if ( ! in_word & & ! is_ascii_space ( c ) )
2021-05-25 20:58:55 -04:00
in_word = true ;
}
if ( in_word )
word_count + + ;
return word_count ;
}
2021-09-18 16:31:47 -04:00
size_t TextEditor : : number_of_words ( ) const
{
if ( document ( ) . is_empty ( ) )
return 0 ;
size_t word_count = 0 ;
bool in_word = false ;
auto text = this - > text ( ) ;
for ( char c : text ) {
if ( in_word & & is_ascii_space ( c ) ) {
in_word = false ;
word_count + + ;
continue ;
}
if ( ! in_word & & ! is_ascii_space ( c ) )
in_word = true ;
}
if ( in_word )
word_count + + ;
return word_count ;
}
2020-02-02 15:07:41 +01:00
void TextEditor : : delete_selection ( )
2019-03-08 14:08:15 +01:00
{
2019-03-08 18:28:24 +01:00
auto selection = normalized_selection ( ) ;
2021-05-02 18:33:31 +02:00
auto selected = selected_text ( ) ;
2019-03-08 18:28:24 +01:00
m_selection . clear ( ) ;
2021-05-02 18:33:31 +02:00
execute < RemoveTextCommand > ( selected , selection ) ;
2019-04-12 02:52:34 +02:00
did_update_selection ( ) ;
2019-04-25 01:28:17 +02:00
did_change ( ) ;
2019-03-08 18:28:24 +01:00
set_cursor ( selection . start ( ) ) ;
2019-03-08 14:08:15 +01:00
update ( ) ;
}
2021-01-27 21:57:39 +10:00
void TextEditor : : delete_text_range ( TextRange range )
{
auto normalized_range = range . normalized ( ) ;
execute < RemoveTextCommand > ( document ( ) . text_in_range ( normalized_range ) , normalized_range ) ;
did_change ( ) ;
set_cursor ( normalized_range . start ( ) ) ;
update ( ) ;
}
2021-11-11 00:55:02 +01:00
void TextEditor : : insert_at_cursor_or_replace_selection ( StringView text )
2019-03-08 14:08:15 +01:00
{
2020-05-27 19:18:40 +02:00
ReflowDeferrer defer ( * this ) ;
2021-02-23 20:42:32 +01:00
VERIFY ( is_editable ( ) ) ;
2022-07-10 12:02:19 +02:00
if ( has_selection ( ) )
2019-03-08 14:08:15 +01:00
delete_selection ( ) ;
2021-05-15 11:38:53 -05:00
// Check if adding a newline leaves the previous line as just whitespace.
auto const clear_length = m_cursor . column ( ) ;
auto const should_clear_last_line = text = = " \n "
& & clear_length > 0
& & current_line ( ) . leading_spaces ( ) = = clear_length ;
2022-07-10 12:02:19 +02:00
execute < InsertTextCommand > ( text , m_cursor ) ;
2021-05-15 11:38:53 -05:00
if ( should_clear_last_line ) { // If it does leave just whitespace, clear it.
auto const original_cursor_position = cursor ( ) ;
TextPosition start ( original_cursor_position . line ( ) - 1 , 0 ) ;
TextPosition end ( original_cursor_position . line ( ) - 1 , clear_length ) ;
TextRange erased_range ( start , end ) ;
execute < RemoveTextCommand > ( document ( ) . text_in_range ( erased_range ) , erased_range ) ;
set_cursor ( original_cursor_position ) ;
}
2019-03-08 14:08:15 +01:00
}
2022-04-13 16:14:59 +02:00
void TextEditor : : replace_all_text_without_resetting_undo_stack ( StringView text )
2022-04-12 22:46:26 +02:00
{
auto start = GUI : : TextPosition ( 0 , 0 ) ;
auto last_line_index = line_count ( ) - 1 ;
auto end = GUI : : TextPosition ( last_line_index , line ( last_line_index ) . length ( ) ) ;
auto range = GUI : : TextRange ( start , end ) ;
auto normalized_range = range . normalized ( ) ;
2022-04-13 16:14:59 +02:00
execute < ReplaceAllTextCommand > ( text , range , " GML Playground Format Text " ) ;
2022-04-12 22:46:26 +02:00
did_change ( ) ;
set_cursor ( normalized_range . start ( ) ) ;
update ( ) ;
}
2020-02-02 15:07:41 +01:00
void TextEditor : : cut ( )
2019-03-08 14:08:15 +01:00
{
2020-07-15 17:04:49 -04:00
if ( ! is_editable ( ) )
2019-05-08 05:00:28 +02:00
return ;
2019-03-08 14:08:15 +01:00
auto selected_text = this - > selected_text ( ) ;
2021-06-03 02:05:58 -07:00
dbgln_if ( TEXTEDITOR_DEBUG , " Cut: \" {} \" " , selected_text ) ;
2020-09-05 16:16:01 +02:00
Clipboard : : the ( ) . set_plain_text ( selected_text ) ;
2019-03-08 14:08:15 +01:00
delete_selection ( ) ;
}
2020-02-02 15:07:41 +01:00
void TextEditor : : copy ( )
2019-03-08 14:08:15 +01:00
{
auto selected_text = this - > selected_text ( ) ;
2021-06-03 02:05:58 -07:00
dbgln_if ( TEXTEDITOR_DEBUG , " Copy: \" {} \" \n " , selected_text ) ;
2020-09-05 16:16:01 +02:00
Clipboard : : the ( ) . set_plain_text ( selected_text ) ;
2019-03-08 14:08:15 +01:00
}
2020-02-02 15:07:41 +01:00
void TextEditor : : paste ( )
2019-03-08 14:08:15 +01:00
{
2020-07-15 17:04:49 -04:00
if ( ! is_editable ( ) )
2019-05-08 05:00:28 +02:00
return ;
2020-05-27 19:18:40 +02:00
2021-11-20 15:22:01 +01:00
auto [ data , mime_type , _ ] = GUI : : Clipboard : : the ( ) . fetch_data_and_type ( ) ;
2022-07-11 17:32:29 +00:00
if ( ! mime_type . starts_with ( " text/ " sv ) )
2021-07-25 17:59:30 +08:00
return ;
2021-11-20 14:59:58 +01:00
if ( data . is_empty ( ) )
2021-05-22 17:29:32 -03:00
return ;
2021-11-20 14:59:58 +01:00
dbgln_if ( TEXTEDITOR_DEBUG , " Paste: \" {} \" " , String : : copy ( data ) ) ;
2020-01-23 21:07:37 +01:00
TemporaryChange change ( m_automatic_indentation_enabled , false ) ;
2021-11-20 14:59:58 +01:00
insert_at_cursor_or_replace_selection ( data ) ;
2019-03-08 14:08:15 +01:00
}
2019-03-31 23:52:02 +02:00
2020-05-27 19:18:40 +02:00
void TextEditor : : defer_reflow ( )
{
+ + m_reflow_deferred ;
}
void TextEditor : : undefer_reflow ( )
{
2021-02-23 20:42:32 +01:00
VERIFY ( m_reflow_deferred ) ;
2020-05-27 19:18:40 +02:00
if ( ! - - m_reflow_deferred ) {
2020-05-29 15:43:06 -04:00
if ( m_reflow_requested ) {
2020-05-27 19:18:40 +02:00
recompute_all_visual_lines ( ) ;
2020-05-29 15:43:06 -04:00
scroll_cursor_into_view ( ) ;
}
2020-05-27 19:18:40 +02:00
}
}
2021-10-27 23:13:49 -05:00
void TextEditor : : try_show_autocomplete ( UserRequestedAutocomplete user_requested_autocomplete )
2021-10-28 02:04:33 -05:00
{
force_update_autocomplete ( [ & , user_requested_autocomplete = move ( user_requested_autocomplete ) ] {
if ( user_requested_autocomplete = = Yes | | m_autocomplete_box - > has_suggestions ( ) ) {
auto position = content_rect_for_position ( cursor ( ) ) . translated ( 0 , - visible_content_rect ( ) . y ( ) ) . bottom_right ( ) . translated ( screen_relative_rect ( ) . top_left ( ) . translated ( ruler_width ( ) , 0 ) . translated ( 10 , 5 ) ) ;
m_autocomplete_box - > show ( position ) ;
}
} ) ;
}
void TextEditor : : try_update_autocomplete ( Function < void ( ) > callback )
{
if ( m_autocomplete_box & & m_autocomplete_box - > is_visible ( ) )
force_update_autocomplete ( move ( callback ) ) ;
}
void TextEditor : : force_update_autocomplete ( Function < void ( ) > callback )
2021-01-01 17:29:11 +03:30
{
if ( m_autocomplete_provider ) {
2021-10-28 02:04:33 -05:00
m_autocomplete_provider - > provide_completions ( [ & , callback = move ( callback ) ] ( auto completions ) {
2021-01-01 17:29:11 +03:30
m_autocomplete_box - > update_suggestions ( move ( completions ) ) ;
2021-10-28 02:04:33 -05:00
if ( callback )
callback ( ) ;
2021-01-01 17:29:11 +03:30
} ) ;
}
}
2021-10-27 23:39:55 -05:00
void TextEditor : : hide_autocomplete_if_needed ( )
{
2021-10-28 20:23:58 -05:00
if ( ! m_should_keep_autocomplete_box )
hide_autocomplete ( ) ;
}
void TextEditor : : hide_autocomplete ( )
{
if ( m_autocomplete_box ) {
2021-10-27 23:39:55 -05:00
m_autocomplete_box - > close ( ) ;
if ( m_autocomplete_timer )
m_autocomplete_timer - > stop ( ) ;
}
}
2020-02-02 15:07:41 +01:00
void TextEditor : : enter_event ( Core : : Event & )
2019-03-31 23:52:02 +02:00
{
2020-04-20 21:31:49 +02:00
m_automatic_selection_scroll_timer - > stop ( ) ;
2019-03-31 23:52:02 +02:00
}
2020-02-02 15:07:41 +01:00
void TextEditor : : leave_event ( Core : : Event & )
2019-03-31 23:52:02 +02:00
{
2020-04-20 21:31:49 +02:00
if ( m_in_drag_select )
m_automatic_selection_scroll_timer - > start ( ) ;
2019-03-31 23:52:02 +02:00
}
2019-04-09 16:20:36 +02:00
2021-09-21 17:02:48 -04:00
void TextEditor : : did_change ( AllowCallback allow_callback )
2019-04-09 16:20:36 +02:00
{
update_content_size ( ) ;
2019-08-25 08:43:01 +02:00
recompute_all_visual_lines ( ) ;
2021-10-27 23:39:55 -05:00
hide_autocomplete_if_needed ( ) ;
2021-06-29 11:22:57 +02:00
m_needs_rehighlight = true ;
2022-04-29 16:48:55 +01:00
if ( on_change & & allow_callback = = AllowCallback : : Yes )
on_change ( ) ;
2019-04-09 16:20:36 +02:00
}
2020-07-14 17:02:46 -04:00
void TextEditor : : set_mode ( const Mode mode )
{
if ( m_mode = = mode )
return ;
m_mode = mode ;
switch ( mode ) {
case Editable :
2021-08-02 11:10:56 +02:00
m_cut_action - > set_enabled ( has_selection ( ) & & ! text_is_secret ( ) ) ;
2020-07-14 17:02:46 -04:00
m_paste_action - > set_enabled ( true ) ;
2022-09-08 18:06:08 -04:00
m_insert_emoji_action - > set_enabled ( true ) ;
2020-07-14 17:02:46 -04:00
set_accepts_emoji_input ( true ) ;
break ;
case DisplayOnly :
2020-07-15 17:04:49 -04:00
case ReadOnly :
2021-08-02 13:19:42 +02:00
m_cut_action - > set_enabled ( false ) ;
2020-07-14 17:02:46 -04:00
m_paste_action - > set_enabled ( false ) ;
2022-09-08 18:06:08 -04:00
m_insert_emoji_action - > set_enabled ( false ) ;
2020-07-14 17:02:46 -04:00
set_accepts_emoji_input ( false ) ;
break ;
default :
2021-02-23 20:42:32 +01:00
VERIFY_NOT_REACHED ( ) ;
2020-07-14 17:02:46 -04:00
}
2020-09-11 14:25:48 +02:00
2022-08-18 15:38:06 +02:00
set_editing_cursor ( ) ;
}
void TextEditor : : set_editing_cursor ( )
{
2020-09-11 14:25:48 +02:00
if ( ! is_displayonly ( ) )
set_override_cursor ( Gfx : : StandardCursor : : IBeam ) ;
else
set_override_cursor ( Gfx : : StandardCursor : : None ) ;
2020-07-14 17:02:46 -04:00
}
2019-04-12 02:52:34 +02:00
2020-02-02 15:07:41 +01:00
void TextEditor : : did_update_selection ( )
2019-04-12 02:52:34 +02:00
{
2021-08-02 11:10:56 +02:00
m_cut_action - > set_enabled ( is_editable ( ) & & has_selection ( ) & & ! text_is_secret ( ) ) ;
m_copy_action - > set_enabled ( has_selection ( ) & & ! text_is_secret ( ) ) ;
2019-04-12 02:52:34 +02:00
if ( on_selection_change )
on_selection_change ( ) ;
2021-01-09 22:47:48 +10:00
if ( is_wrapping_enabled ( ) ) {
2019-08-25 10:23:11 +02:00
// FIXME: Try to repaint less.
update ( ) ;
}
2019-04-12 02:52:34 +02:00
}
2019-04-18 12:25:00 +02:00
2020-02-02 15:07:41 +01:00
void TextEditor : : context_menu_event ( ContextMenuEvent & event )
2019-04-18 12:25:00 +02:00
{
2020-07-14 17:02:46 -04:00
if ( is_displayonly ( ) )
return ;
2022-09-09 07:21:31 -04:00
m_insert_emoji_action - > set_enabled ( accepts_emoji_input ( ) & & ! window ( ) - > blocks_emoji_input ( ) ) ;
2019-04-18 12:25:00 +02:00
if ( ! m_context_menu ) {
2020-02-02 15:07:41 +01:00
m_context_menu = Menu : : construct ( ) ;
2019-04-18 12:25:00 +02:00
m_context_menu - > add_action ( undo_action ( ) ) ;
m_context_menu - > add_action ( redo_action ( ) ) ;
m_context_menu - > add_separator ( ) ;
m_context_menu - > add_action ( cut_action ( ) ) ;
m_context_menu - > add_action ( copy_action ( ) ) ;
m_context_menu - > add_action ( paste_action ( ) ) ;
2020-04-20 21:10:03 +02:00
m_context_menu - > add_separator ( ) ;
m_context_menu - > add_action ( select_all_action ( ) ) ;
2022-09-08 18:06:08 -04:00
m_context_menu - > add_action ( insert_emoji_action ( ) ) ;
2020-01-23 21:21:55 +01:00
if ( is_multi_line ( ) ) {
m_context_menu - > add_separator ( ) ;
2020-01-23 21:27:27 +01:00
m_context_menu - > add_action ( go_to_line_action ( ) ) ;
2020-01-23 21:21:55 +01:00
}
2019-08-25 21:33:08 +02:00
if ( ! m_custom_context_menu_actions . is_empty ( ) ) {
m_context_menu - > add_separator ( ) ;
for ( auto & action : m_custom_context_menu_actions ) {
m_context_menu - > add_action ( action ) ;
}
}
2019-04-18 12:25:00 +02:00
}
m_context_menu - > popup ( event . screen_position ( ) ) ;
}
2019-04-24 22:24:16 +02:00
2020-02-06 11:56:38 +01:00
void TextEditor : : set_text_alignment ( Gfx : : TextAlignment alignment )
2019-04-24 22:24:16 +02:00
{
if ( m_text_alignment = = alignment )
return ;
m_text_alignment = alignment ;
update ( ) ;
}
2019-04-24 23:42:49 +02:00
2020-02-02 15:07:41 +01:00
void TextEditor : : resize_event ( ResizeEvent & event )
2019-04-24 23:42:49 +02:00
{
2021-05-03 20:31:58 +02:00
AbstractScrollableWidget : : resize_event ( event ) ;
2019-04-24 23:42:49 +02:00
update_content_size ( ) ;
2019-08-25 08:43:01 +02:00
recompute_all_visual_lines ( ) ;
2019-04-24 23:42:49 +02:00
}
2019-08-21 21:23:17 +02:00
2020-03-16 13:36:21 +02:00
void TextEditor : : theme_change_event ( ThemeChangeEvent & event )
{
2021-05-03 20:31:58 +02:00
AbstractScrollableWidget : : theme_change_event ( event ) ;
2021-06-29 11:22:57 +02:00
m_needs_rehighlight = true ;
2020-03-16 13:36:21 +02:00
}
2021-07-09 21:34:20 +10:00
void TextEditor : : set_selection ( TextRange const & selection )
2019-08-21 21:23:17 +02:00
{
if ( m_selection = = selection )
return ;
m_selection = selection ;
set_cursor ( m_selection . end ( ) ) ;
scroll_position_into_view ( normalized_selection ( ) . start ( ) ) ;
update ( ) ;
}
2020-02-02 15:07:41 +01:00
void TextEditor : : clear_selection ( )
2019-12-01 18:48:32 -08:00
{
if ( ! has_selection ( ) )
return ;
m_selection . clear ( ) ;
update ( ) ;
}
2020-02-02 15:07:41 +01:00
void TextEditor : : recompute_all_visual_lines ( )
2019-08-25 08:43:01 +02:00
{
2020-05-27 19:18:40 +02:00
if ( m_reflow_deferred ) {
m_reflow_requested = true ;
return ;
}
2020-06-13 00:31:46 +02:00
m_reflow_requested = false ;
2019-08-25 08:43:01 +02:00
int y_offset = 0 ;
2019-12-09 17:45:40 +01:00
for ( size_t line_index = 0 ; line_index < line_count ( ) ; + + line_index ) {
2019-10-27 18:00:07 +01:00
recompute_visual_lines ( line_index ) ;
m_line_visual_data [ line_index ] . visual_rect . set_y ( y_offset ) ;
y_offset + = m_line_visual_data [ line_index ] . visual_rect . height ( ) ;
2019-08-25 08:43:01 +02:00
}
2019-08-28 23:26:24 -05:00
update_content_size ( ) ;
2019-08-25 08:43:01 +02:00
}
2020-02-02 15:07:41 +01:00
void TextEditor : : ensure_cursor_is_valid ( )
2019-11-05 16:37:47 -06:00
{
2019-11-30 16:50:24 +01:00
auto new_cursor = m_cursor ;
2019-12-09 17:45:40 +01:00
if ( new_cursor . line ( ) > = line_count ( ) )
new_cursor . set_line ( line_count ( ) - 1 ) ;
if ( new_cursor . column ( ) > line ( new_cursor . line ( ) ) . length ( ) )
new_cursor . set_column ( line ( new_cursor . line ( ) ) . length ( ) ) ;
2019-11-30 16:50:24 +01:00
if ( m_cursor ! = new_cursor )
set_cursor ( new_cursor ) ;
2019-11-05 16:37:47 -06:00
}
2020-02-02 15:07:41 +01:00
size_t TextEditor : : visual_line_containing ( size_t line_index , size_t column ) const
2019-10-27 16:10:07 +01:00
{
2019-12-09 17:45:40 +01:00
size_t visual_line_index = 0 ;
2021-07-09 21:34:20 +10:00
for_each_visual_line ( line_index , [ & ] ( Gfx : : IntRect const & , auto & view , size_t start_of_visual_line , [[maybe_unused]] bool is_last_visual_line ) {
2019-10-27 16:10:07 +01:00
if ( column > = start_of_visual_line & & ( ( column - start_of_visual_line ) < view . length ( ) ) )
return IterationDecision : : Break ;
+ + visual_line_index ;
return IterationDecision : : Continue ;
} ) ;
return visual_line_index ;
}
2020-02-02 15:07:41 +01:00
void TextEditor : : recompute_visual_lines ( size_t line_index )
2019-08-25 08:43:01 +02:00
{
2019-10-27 18:00:07 +01:00
auto & line = document ( ) . line ( line_index ) ;
auto & visual_data = m_line_visual_data [ line_index ] ;
2019-08-25 08:43:01 +02:00
2019-10-27 18:00:07 +01:00
visual_data . visual_line_breaks . clear_with_capacity ( ) ;
2019-08-25 08:43:01 +02:00
2019-10-27 18:00:07 +01:00
int available_width = visible_text_rect_in_inner_coordinates ( ) . width ( ) ;
2021-01-09 22:47:48 +10:00
if ( is_wrapping_enabled ( ) ) {
2019-08-25 08:43:01 +02:00
int line_width_so_far = 0 ;
2021-01-09 22:47:48 +10:00
size_t last_whitespace_index = 0 ;
size_t line_width_since_last_whitespace = 0 ;
2020-05-18 16:38:28 +02:00
auto glyph_spacing = font ( ) . glyph_spacing ( ) ;
2019-12-09 17:45:40 +01:00
for ( size_t i = 0 ; i < line . length ( ) ; + + i ) {
2020-08-05 16:31:20 -04:00
auto code_point = line . code_points ( ) [ i ] ;
2021-06-01 21:18:08 +02:00
if ( is_ascii_space ( code_point ) ) {
2021-01-09 22:47:48 +10:00
last_whitespace_index = i ;
line_width_since_last_whitespace = 0 ;
}
2020-08-05 16:31:20 -04:00
auto glyph_width = font ( ) . glyph_or_emoji_width ( code_point ) ;
2021-01-09 22:47:48 +10:00
line_width_since_last_whitespace + = glyph_width + glyph_spacing ;
2020-05-18 16:38:28 +02:00
if ( ( line_width_so_far + glyph_width + glyph_spacing ) > available_width ) {
2021-01-09 22:47:48 +10:00
if ( m_wrapping_mode = = WrappingMode : : WrapAtWords & & last_whitespace_index ! = 0 ) {
// Plus 1 to get the first letter of the word.
visual_data . visual_line_breaks . append ( last_whitespace_index + 1 ) ;
line_width_so_far = line_width_since_last_whitespace ;
last_whitespace_index = 0 ;
line_width_since_last_whitespace = 0 ;
} else {
visual_data . visual_line_breaks . append ( i ) ;
line_width_so_far = glyph_width + glyph_spacing ;
}
2019-08-25 08:43:01 +02:00
continue ;
}
2020-05-18 16:38:28 +02:00
line_width_so_far + = glyph_width + glyph_spacing ;
2019-08-25 08:43:01 +02:00
}
}
2019-10-27 18:00:07 +01:00
visual_data . visual_line_breaks . append ( line . length ( ) ) ;
2019-08-25 08:43:01 +02:00
2021-01-09 22:47:48 +10:00
if ( is_wrapping_enabled ( ) )
2020-02-25 14:49:47 +01:00
visual_data . visual_rect = { m_horizontal_content_padding , 0 , available_width , static_cast < int > ( visual_data . visual_line_breaks . size ( ) ) * line_height ( ) } ;
2019-08-25 08:43:01 +02:00
else
2021-06-25 17:48:51 +02:00
visual_data . visual_rect = { m_horizontal_content_padding , 0 , text_width_for_font ( line . view ( ) , font ( ) ) , line_height ( ) } ;
2019-08-25 08:43:01 +02:00
}
template < typename Callback >
2020-02-02 15:07:41 +01:00
void TextEditor : : for_each_visual_line ( size_t line_index , Callback callback ) const
2019-08-25 08:43:01 +02:00
{
2019-10-27 18:00:07 +01:00
auto editor_visible_text_rect = visible_text_rect_in_inner_coordinates ( ) ;
2019-12-09 17:45:40 +01:00
size_t start_of_line = 0 ;
size_t visual_line_index = 0 ;
2019-10-27 18:00:07 +01:00
auto & line = document ( ) . line ( line_index ) ;
auto & visual_data = m_line_visual_data [ line_index ] ;
for ( auto visual_line_break : visual_data . visual_line_breaks ) {
2020-08-05 16:31:20 -04:00
auto visual_line_view = Utf32View ( line . code_points ( ) + start_of_line , visual_line_break - start_of_line ) ;
2020-06-10 10:57:59 +02:00
Gfx : : IntRect visual_line_rect {
2019-10-27 18:00:07 +01:00
visual_data . visual_rect . x ( ) ,
2019-12-09 17:45:40 +01:00
visual_data . visual_rect . y ( ) + ( ( int ) visual_line_index * line_height ( ) ) ,
2021-06-25 17:48:51 +02:00
text_width_for_font ( visual_line_view , font ( ) ) + font ( ) . glyph_spacing ( ) ,
2019-10-27 18:00:07 +01:00
line_height ( )
2019-08-25 08:43:01 +02:00
} ;
2019-10-27 18:00:07 +01:00
if ( is_right_text_alignment ( text_alignment ( ) ) )
2019-09-16 20:57:32 +02:00
visual_line_rect . set_right_without_resize ( editor_visible_text_rect . right ( ) ) ;
2020-06-29 20:34:42 +02:00
if ( is_single_line ( ) ) {
2019-09-16 20:57:32 +02:00
visual_line_rect . center_vertically_within ( editor_visible_text_rect ) ;
2020-06-29 20:34:42 +02:00
if ( m_icon )
2021-04-12 11:47:09 -07:00
visual_line_rect . translate_by ( icon_size ( ) + icon_padding ( ) , 0 ) ;
2020-06-29 20:34:42 +02:00
}
2020-11-13 15:29:55 +10:00
if ( callback ( visual_line_rect , visual_line_view , start_of_line , visual_line_index = = visual_data . visual_line_breaks . size ( ) - 1 ) = = IterationDecision : : Break )
2019-08-25 08:43:01 +02:00
break ;
start_of_line = visual_line_break ;
2019-10-27 18:00:07 +01:00
+ + visual_line_index ;
2019-08-25 08:43:01 +02:00
}
}
2019-08-25 12:23:14 +02:00
2021-01-09 22:47:48 +10:00
void TextEditor : : set_wrapping_mode ( WrappingMode mode )
2019-08-25 12:23:14 +02:00
{
2021-01-09 22:47:48 +10:00
if ( m_wrapping_mode = = mode )
2019-08-25 12:23:14 +02:00
return ;
2021-01-09 22:47:48 +10:00
m_wrapping_mode = mode ;
horizontal_scrollbar ( ) . set_visible ( m_wrapping_mode = = WrappingMode : : NoWrap ) ;
2019-08-25 12:23:14 +02:00
update_content_size ( ) ;
recompute_all_visual_lines ( ) ;
update ( ) ;
}
2019-08-25 14:04:46 +02:00
2020-02-02 15:07:41 +01:00
void TextEditor : : add_custom_context_menu_action ( Action & action )
2019-08-25 21:33:08 +02:00
{
m_custom_context_menu_actions . append ( action ) ;
}
2019-09-01 12:26:35 +02:00
2020-02-02 15:07:41 +01:00
void TextEditor : : did_change_font ( )
2019-09-01 12:26:35 +02:00
{
vertical_scrollbar ( ) . set_step ( line_height ( ) ) ;
2019-12-29 23:03:41 +01:00
recompute_all_visual_lines ( ) ;
update ( ) ;
2021-05-03 20:31:58 +02:00
AbstractScrollableWidget : : did_change_font ( ) ;
2019-09-01 12:26:35 +02:00
}
2019-10-27 18:00:07 +01:00
2020-02-02 15:07:41 +01:00
void TextEditor : : document_did_append_line ( )
2019-10-27 18:00:07 +01:00
{
m_line_visual_data . append ( make < LineVisualData > ( ) ) ;
2019-10-27 19:36:59 +01:00
recompute_all_visual_lines ( ) ;
update ( ) ;
2019-10-27 18:00:07 +01:00
}
2020-02-02 15:07:41 +01:00
void TextEditor : : document_did_remove_line ( size_t line_index )
2019-10-27 18:00:07 +01:00
{
m_line_visual_data . remove ( line_index ) ;
2019-10-27 19:36:59 +01:00
recompute_all_visual_lines ( ) ;
update ( ) ;
2019-10-27 18:00:07 +01:00
}
2020-02-02 15:07:41 +01:00
void TextEditor : : document_did_remove_all_lines ( )
2019-10-27 18:00:07 +01:00
{
m_line_visual_data . clear ( ) ;
2019-10-27 19:36:59 +01:00
recompute_all_visual_lines ( ) ;
update ( ) ;
2019-10-27 18:00:07 +01:00
}
2020-02-02 15:07:41 +01:00
void TextEditor : : document_did_insert_line ( size_t line_index )
2019-10-27 18:00:07 +01:00
{
m_line_visual_data . insert ( line_index , make < LineVisualData > ( ) ) ;
2019-10-27 19:36:59 +01:00
recompute_all_visual_lines ( ) ;
update ( ) ;
}
2021-09-21 17:02:48 -04:00
void TextEditor : : document_did_change ( AllowCallback allow_callback )
2019-10-27 19:36:59 +01:00
{
2021-09-21 17:02:48 -04:00
did_change ( allow_callback ) ;
2019-10-27 19:36:59 +01:00
update ( ) ;
2019-10-27 18:00:07 +01:00
}
2021-05-08 13:16:37 +02:00
void TextEditor : : document_did_update_undo_stack ( )
{
2021-05-08 21:44:22 +02:00
auto make_action_text = [ ] ( auto prefix , auto suffix ) {
StringBuilder builder ;
builder . append ( prefix ) ;
if ( suffix . has_value ( ) ) {
builder . append ( ' ' ) ;
builder . append ( suffix . value ( ) ) ;
}
return builder . to_string ( ) ;
} ;
2021-08-02 11:10:56 +02:00
m_undo_action - > set_enabled ( can_undo ( ) & & ! text_is_secret ( ) ) ;
m_redo_action - > set_enabled ( can_redo ( ) & & ! text_is_secret ( ) ) ;
2021-05-08 13:40:33 +02:00
2022-07-11 17:32:29 +00:00
m_undo_action - > set_text ( make_action_text ( " &Undo " sv , document ( ) . undo_stack ( ) . undo_action_text ( ) ) ) ;
m_redo_action - > set_text ( make_action_text ( " &Redo " sv , document ( ) . undo_stack ( ) . redo_action_text ( ) ) ) ;
2021-05-08 21:44:22 +02:00
2021-05-08 13:40:33 +02:00
// FIXME: This is currently firing more often than it should.
// Ideally we'd only send this out when the undo stack modified state actually changes.
if ( on_modified_change )
on_modified_change ( document ( ) . is_modified ( ) ) ;
2021-05-08 13:16:37 +02:00
}
2021-09-21 17:02:48 -04:00
void TextEditor : : document_did_set_text ( AllowCallback allow_callback )
2019-11-23 17:41:14 +01:00
{
m_line_visual_data . clear ( ) ;
2019-12-09 17:45:40 +01:00
for ( size_t i = 0 ; i < m_document - > line_count ( ) ; + + i )
2019-11-23 17:41:14 +01:00
m_line_visual_data . append ( make < LineVisualData > ( ) ) ;
2021-09-21 17:02:48 -04:00
document_did_change ( allow_callback ) ;
2019-11-23 17:41:14 +01:00
}
2021-07-09 21:34:20 +10:00
void TextEditor : : document_did_set_cursor ( TextPosition const & position )
2019-11-30 13:05:17 +01:00
{
set_cursor ( position ) ;
}
2021-10-27 23:39:55 -05:00
void TextEditor : : cursor_did_change ( )
{
hide_autocomplete_if_needed ( ) ;
}
2021-07-27 03:19:56 +08:00
void TextEditor : : clipboard_content_did_change ( String const & mime_type )
{
2022-07-11 17:32:29 +00:00
m_paste_action - > set_enabled ( is_editable ( ) & & mime_type . starts_with ( " text/ " sv ) ) ;
2021-07-27 03:19:56 +08:00
}
2020-02-02 15:07:41 +01:00
void TextEditor : : set_document ( TextDocument & document )
2019-10-27 19:36:59 +01:00
{
if ( m_document . ptr ( ) = = & document )
return ;
if ( m_document )
m_document - > unregister_client ( * this ) ;
m_document = document ;
m_line_visual_data . clear ( ) ;
2019-12-09 17:45:40 +01:00
for ( size_t i = 0 ; i < m_document - > line_count ( ) ; + + i ) {
2019-10-27 19:36:59 +01:00
m_line_visual_data . append ( make < LineVisualData > ( ) ) ;
}
2020-08-05 17:19:37 +02:00
set_cursor ( 0 , 0 ) ;
2019-12-01 18:49:13 -08:00
if ( has_selection ( ) )
m_selection . clear ( ) ;
2019-10-27 19:36:59 +01:00
recompute_all_visual_lines ( ) ;
update ( ) ;
m_document - > register_client ( * this ) ;
}
2019-11-02 14:22:49 -05:00
2021-06-29 11:22:57 +02:00
void TextEditor : : rehighlight_if_needed ( )
2019-11-08 19:49:08 +01:00
{
2021-06-29 11:22:57 +02:00
if ( ! m_needs_rehighlight )
2019-11-08 19:49:08 +01:00
return ;
2022-02-07 08:08:09 +02:00
force_rehighlight ( ) ;
}
void TextEditor : : force_rehighlight ( )
{
2020-02-07 20:07:15 +01:00
if ( m_highlighter )
2020-03-16 01:05:06 +02:00
m_highlighter - > rehighlight ( palette ( ) ) ;
2021-06-29 11:22:57 +02:00
m_needs_rehighlight = false ;
2019-11-08 19:49:08 +01:00
}
2020-02-02 15:07:41 +01:00
2021-07-09 21:34:20 +10:00
Syntax : : Highlighter const * TextEditor : : syntax_highlighter ( ) const
2020-03-12 16:36:25 +02:00
{
2020-03-12 17:23:54 +02:00
return m_highlighter . ptr ( ) ;
2020-03-12 16:36:25 +02:00
}
2022-02-07 08:08:09 +02:00
Syntax : : Highlighter * TextEditor : : syntax_highlighter ( )
{
return m_highlighter . ptr ( ) ;
}
2021-02-07 15:15:10 +01:00
void TextEditor : : set_syntax_highlighter ( OwnPtr < Syntax : : Highlighter > highlighter )
2020-02-07 20:07:15 +01:00
{
if ( m_highlighter )
m_highlighter - > detach ( ) ;
m_highlighter = move ( highlighter ) ;
if ( m_highlighter ) {
m_highlighter - > attach ( * this ) ;
2021-06-29 11:22:57 +02:00
m_needs_rehighlight = true ;
2020-03-11 03:01:52 +02:00
} else
2022-03-29 16:31:26 +03:00
document ( ) . set_spans ( Syntax : : HighlighterClient : : span_collection_index , { } ) ;
2022-02-22 15:00:56 -05:00
if ( on_highlighter_change )
on_highlighter_change ( ) ;
2020-02-07 20:07:15 +01:00
}
2021-07-09 21:34:20 +10:00
AutocompleteProvider const * TextEditor : : autocomplete_provider ( ) const
2020-12-30 13:55:06 +03:30
{
return m_autocomplete_provider . ptr ( ) ;
}
void TextEditor : : set_autocomplete_provider ( OwnPtr < AutocompleteProvider > & & provider )
{
if ( m_autocomplete_provider )
m_autocomplete_provider - > detach ( ) ;
m_autocomplete_provider = move ( provider ) ;
if ( m_autocomplete_provider ) {
m_autocomplete_provider - > attach ( * this ) ;
if ( ! m_autocomplete_box )
m_autocomplete_box = make < AutocompleteBox > ( * this ) ;
}
if ( m_autocomplete_box )
2021-10-28 20:23:58 -05:00
hide_autocomplete ( ) ;
2020-12-30 13:55:06 +03:30
}
2021-07-09 21:34:20 +10:00
EditingEngine const * TextEditor : : editing_engine ( ) const
2021-01-02 11:59:55 +01:00
{
return m_editing_engine . ptr ( ) ;
}
void TextEditor : : set_editing_engine ( OwnPtr < EditingEngine > editing_engine )
{
if ( m_editing_engine )
m_editing_engine - > detach ( ) ;
m_editing_engine = move ( editing_engine ) ;
2021-02-23 20:42:32 +01:00
VERIFY ( m_editing_engine ) ;
2021-01-02 11:59:55 +01:00
m_editing_engine - > attach ( * this ) ;
m_cursor_state = true ;
update_cursor ( ) ;
stop_timer ( ) ;
start_timer ( 500 ) ;
}
2020-02-15 00:24:14 +01:00
int TextEditor : : line_height ( ) const
{
2022-02-24 08:52:31 -05:00
return font ( ) . preferred_line_height ( ) ;
2020-02-15 00:24:14 +01:00
}
2020-05-18 16:38:28 +02:00
int TextEditor : : fixed_glyph_width ( ) const
2020-02-15 00:24:14 +01:00
{
2021-02-23 20:42:32 +01:00
VERIFY ( font ( ) . is_fixed_width ( ) ) ;
2020-05-18 16:38:28 +02:00
return font ( ) . glyph_width ( ' ' ) ;
2020-02-15 00:24:14 +01:00
}
2021-07-09 21:34:20 +10:00
void TextEditor : : set_icon ( Gfx : : Bitmap const * icon )
2020-06-29 20:34:42 +02:00
{
if ( m_icon = = icon )
return ;
m_icon = icon ;
update ( ) ;
}
2020-09-01 19:10:55 +02:00
void TextEditor : : set_visualize_trailing_whitespace ( bool enabled )
{
if ( m_visualize_trailing_whitespace = = enabled )
return ;
m_visualize_trailing_whitespace = enabled ;
update ( ) ;
}
2021-03-17 13:52:42 -03:00
void TextEditor : : set_visualize_leading_whitespace ( bool enabled )
{
if ( m_visualize_leading_whitespace = = enabled )
return ;
m_visualize_leading_whitespace = enabled ;
update ( ) ;
}
2021-01-01 17:29:11 +03:30
void TextEditor : : set_should_autocomplete_automatically ( bool value )
{
if ( value = = should_autocomplete_automatically ( ) )
return ;
if ( value ) {
2021-02-23 20:42:32 +01:00
VERIFY ( m_autocomplete_provider ) ;
2021-10-28 20:41:56 -05:00
m_autocomplete_timer = Core : : Timer : : create_single_shot ( m_automatic_autocomplete_delay_ms , [ this ] {
if ( m_autocomplete_box & & ! m_autocomplete_box - > is_visible ( ) )
try_show_autocomplete ( UserRequestedAutocomplete : : No ) ;
} ) ;
2021-01-01 17:29:11 +03:30
return ;
}
remove_child ( * m_autocomplete_timer ) ;
m_autocomplete_timer = nullptr ;
}
2021-06-25 17:48:51 +02:00
2022-05-07 16:03:33 +02:00
void TextEditor : : set_substitution_code_point ( Optional < u32 > code_point )
2021-06-25 17:48:51 +02:00
{
2022-05-07 16:03:33 +02:00
if ( code_point . has_value ( ) )
VERIFY ( is_unicode ( code_point . value ( ) ) ) ;
2021-06-25 17:48:51 +02:00
m_substitution_string_data . clear ( ) ;
2022-05-07 16:03:33 +02:00
m_substitution_code_point = move ( code_point ) ;
2021-06-25 17:48:51 +02:00
}
2021-01-02 11:59:55 +01:00
int TextEditor : : number_of_visible_lines ( ) const
{
return visible_content_rect ( ) . height ( ) / line_height ( ) ;
}
2021-01-01 17:29:11 +03:30
2021-04-10 00:09:44 +02:00
void TextEditor : : set_ruler_visible ( bool visible )
{
if ( m_ruler_visible = = visible )
return ;
m_ruler_visible = visible ;
recompute_all_visual_lines ( ) ;
update ( ) ;
}
2021-06-12 04:50:23 +03:00
void TextEditor : : set_gutter_visible ( bool visible )
{
if ( m_gutter_visible = = visible )
return ;
m_gutter_visible = visible ;
recompute_all_visual_lines ( ) ;
update ( ) ;
}
2021-09-11 11:08:01 -04:00
void TextEditor : : set_cursor_line_highlighting ( bool highlighted )
{
if ( m_cursor_line_highlighting = = highlighted )
return ;
m_cursor_line_highlighting = highlighted ;
update ( ) ;
}
2021-05-08 23:38:01 +02:00
void TextEditor : : undo ( )
{
clear_selection ( ) ;
document ( ) . undo ( ) ;
}
void TextEditor : : redo ( )
{
clear_selection ( ) ;
document ( ) . redo ( ) ;
}
2021-08-02 11:10:56 +02:00
void TextEditor : : set_text_is_secret ( bool text_is_secret )
{
m_text_is_secret = text_is_secret ;
document_did_update_undo_stack ( ) ;
did_update_selection ( ) ;
}
2022-03-29 16:33:46 +03:00
TextRange TextEditor : : find_text ( StringView needle , SearchDirection direction , GUI : : TextDocument : : SearchShouldWrap should_wrap , bool use_regex , bool match_case )
{
GUI : : TextRange range { } ;
if ( direction = = SearchDirection : : Forward ) {
range = document ( ) . find_next ( needle ,
m_search_result_index . has_value ( ) ? m_search_results [ * m_search_result_index ] . end ( ) : GUI : : TextPosition { } ,
should_wrap , use_regex , match_case ) ;
} else {
range = document ( ) . find_previous ( needle ,
m_search_result_index . has_value ( ) ? m_search_results [ * m_search_result_index ] . start ( ) : GUI : : TextPosition { } ,
should_wrap , use_regex , match_case ) ;
}
if ( ! range . is_valid ( ) ) {
reset_search_results ( ) ;
return { } ;
}
auto all_results = document ( ) . find_all ( needle , use_regex , match_case ) ;
on_search_results ( range , all_results ) ;
return range ;
}
void TextEditor : : reset_search_results ( )
{
m_search_result_index . clear ( ) ;
m_search_results . clear ( ) ;
document ( ) . set_spans ( search_results_span_collection_index , { } ) ;
update ( ) ;
}
void TextEditor : : on_search_results ( GUI : : TextRange current , Vector < GUI : : TextRange > all_results )
{
m_search_result_index . clear ( ) ;
m_search_results . clear ( ) ;
set_cursor ( current . start ( ) ) ;
if ( auto it = all_results . find ( current ) ; it - > is_valid ( ) )
m_search_result_index = it . index ( ) ;
m_search_results = move ( all_results ) ;
Vector < GUI : : TextDocumentSpan > spans ;
for ( size_t i = 0 ; i < m_search_results . size ( ) ; + + i ) {
auto & result = m_search_results [ i ] ;
GUI : : TextDocumentSpan span ;
span . range = result ;
span . attributes . background_color = palette ( ) . hover_highlight ( ) ;
span . attributes . color = Color : : from_argb ( 0xff000000 ) ; // So text without spans from a highlighter will have color
if ( i = = m_search_result_index ) {
span . attributes . bold = true ;
span . attributes . underline = true ;
}
spans . append ( move ( span ) ) ;
}
document ( ) . set_spans ( search_results_span_collection_index , move ( spans ) ) ;
update ( ) ;
}
2020-02-02 15:07:41 +01:00
}