GTextEditor.cpp 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529
  1. #include <LibGUI/GTextEditor.h>
  2. #include <LibGUI/GScrollBar.h>
  3. #include <LibGUI/GFontDatabase.h>
  4. #include <SharedGraphics/Painter.h>
  5. #include <Kernel/KeyCode.h>
  6. #include <unistd.h>
  7. #include <fcntl.h>
  8. #include <stdio.h>
  9. GTextEditor::GTextEditor(GWidget* parent)
  10. : GWidget(parent)
  11. {
  12. set_font(GFontDatabase::the().get_by_name("Csilla Thin"));
  13. set_fill_with_background_color(false);
  14. m_vertical_scrollbar = new GScrollBar(Orientation::Vertical, this);
  15. m_vertical_scrollbar->set_step(4);
  16. m_vertical_scrollbar->on_change = [this] (int) {
  17. update();
  18. };
  19. m_horizontal_scrollbar = new GScrollBar(Orientation::Horizontal, this);
  20. m_horizontal_scrollbar->set_step(4);
  21. m_horizontal_scrollbar->set_big_step(30);
  22. m_horizontal_scrollbar->on_change = [this] (int) {
  23. update();
  24. };
  25. m_lines.append(make<Line>());
  26. }
  27. GTextEditor::~GTextEditor()
  28. {
  29. }
  30. void GTextEditor::set_text(const String& text)
  31. {
  32. m_lines.clear();
  33. int start_of_current_line = 0;
  34. auto add_line = [&] (int current_position) {
  35. int line_length = current_position - start_of_current_line;
  36. auto line = make<Line>();
  37. if (line_length)
  38. line->set_text(text.substring(start_of_current_line, current_position - start_of_current_line));
  39. m_lines.append(move(line));
  40. start_of_current_line = current_position + 1;
  41. };
  42. int i = 0;
  43. for (i = 0; i < text.length(); ++i) {
  44. if (text[i] == '\n')
  45. add_line(i);
  46. }
  47. add_line(i);
  48. set_cursor(0, 0);
  49. update();
  50. }
  51. void GTextEditor::resize_event(GResizeEvent& event)
  52. {
  53. update_scrollbar_ranges();
  54. m_vertical_scrollbar->set_relative_rect(event.size().width() - m_vertical_scrollbar->preferred_size().width(), 0, m_vertical_scrollbar->preferred_size().width(), event.size().height() - m_horizontal_scrollbar->preferred_size().height());
  55. m_horizontal_scrollbar->set_relative_rect(0, event.size().height() - m_horizontal_scrollbar->preferred_size().height(), event.size().width() - m_vertical_scrollbar->preferred_size().width(), m_horizontal_scrollbar->preferred_size().height());
  56. }
  57. void GTextEditor::update_scrollbar_ranges()
  58. {
  59. int available_height = height() - m_horizontal_scrollbar->height();
  60. int excess_height = max(0, (content_height() + padding() * 2) - available_height);
  61. m_vertical_scrollbar->set_range(0, excess_height);
  62. int available_width = width() - m_vertical_scrollbar->width() - ruler_width();
  63. int excess_width = max(0, (content_width() + padding() * 2) - available_width);
  64. m_horizontal_scrollbar->set_range(0, excess_width);
  65. m_vertical_scrollbar->set_big_step(visible_content_rect().height());
  66. }
  67. int GTextEditor::content_height() const
  68. {
  69. return line_count() * line_height();
  70. }
  71. int GTextEditor::content_width() const
  72. {
  73. // FIXME: Cache this somewhere.
  74. int max_width = 0;
  75. for (auto& line : m_lines)
  76. max_width = max(line->width(font()), max_width);
  77. return max_width;
  78. }
  79. GTextPosition GTextEditor::text_position_at(const Point& a_position) const
  80. {
  81. auto position = a_position;
  82. position.move_by(m_horizontal_scrollbar->value(), m_vertical_scrollbar->value());
  83. position.move_by(-(padding() + ruler_width()), -padding());
  84. int line_index = position.y() / line_height();
  85. int column_index = position.x() / glyph_width();
  86. line_index = min(line_index, line_count() - 1);
  87. column_index = min(column_index, m_lines[line_index]->length());
  88. return { line_index, column_index };
  89. }
  90. void GTextEditor::mousedown_event(GMouseEvent& event)
  91. {
  92. set_cursor(text_position_at(event.position()));
  93. }
  94. int GTextEditor::ruler_width() const
  95. {
  96. // FIXME: Resize based on needed space.
  97. return 5 * font().glyph_width('x') + 4;
  98. }
  99. Rect GTextEditor::ruler_content_rect(int line_index) const
  100. {
  101. return {
  102. 0 - ruler_width() - padding() + m_horizontal_scrollbar->value(),
  103. line_index * line_height(),
  104. ruler_width(),
  105. line_height()
  106. };
  107. }
  108. void GTextEditor::paint_event(GPaintEvent& event)
  109. {
  110. Painter painter(*this);
  111. painter.set_clip_rect(event.rect());
  112. painter.fill_rect(event.rect(), Color::White);
  113. Rect ruler_rect { 0, 0, ruler_width(), height() - m_horizontal_scrollbar->height()};
  114. painter.fill_rect(ruler_rect, Color::LightGray);
  115. painter.draw_line(ruler_rect.top_right(), ruler_rect.bottom_right(), Color::DarkGray);
  116. painter.translate(-m_horizontal_scrollbar->value(), -m_vertical_scrollbar->value());
  117. painter.translate(padding() + ruler_width(), padding());
  118. int exposed_width = max(content_width(), width());
  119. int first_visible_line = text_position_at(event.rect().top_left()).line();
  120. int last_visible_line = text_position_at(event.rect().bottom_right()).line();
  121. painter.set_font(Font::default_font());
  122. for (int i = first_visible_line; i <= last_visible_line; ++i) {
  123. bool is_current_line = i == m_cursor.line();
  124. auto ruler_line_rect = ruler_content_rect(i);
  125. //painter.fill_rect(ruler_line_rect, Color::LightGray);
  126. Color text_color = Color::MidGray;
  127. if (is_current_line) {
  128. painter.set_font(Font::default_bold_font());
  129. text_color = Color::DarkGray;
  130. }
  131. painter.draw_text(ruler_line_rect.shrunken(2, 0), String::format("%u", i), TextAlignment::CenterRight, text_color);
  132. if (is_current_line)
  133. painter.set_font(Font::default_font());
  134. }
  135. painter.set_font(font());
  136. painter.set_clip_rect({ ruler_rect.right() + 1, 0, width() - m_vertical_scrollbar->width() - ruler_width(), height() - m_horizontal_scrollbar->height() });
  137. for (int i = first_visible_line; i <= last_visible_line; ++i) {
  138. auto& line = *m_lines[i];
  139. auto line_rect = line_content_rect(i);
  140. line_rect.set_width(exposed_width);
  141. if (i == m_cursor.line())
  142. painter.fill_rect(line_rect, Color(230, 230, 230));
  143. painter.draw_text(line_rect, line.characters(), line.length(), TextAlignment::CenterLeft, Color::Black);
  144. }
  145. if (is_focused() && m_cursor_state)
  146. painter.fill_rect(cursor_content_rect(), Color::Red);
  147. painter.clear_clip_rect();
  148. painter.set_clip_rect(event.rect());
  149. painter.translate(0 - padding() - ruler_width(), -padding());
  150. painter.translate(m_horizontal_scrollbar->value(), m_vertical_scrollbar->value());
  151. painter.fill_rect({ m_horizontal_scrollbar->relative_rect().top_right().translated(1, 0), { m_vertical_scrollbar->preferred_size().width(), m_horizontal_scrollbar->preferred_size().height() } }, Color::LightGray);
  152. if (is_focused()) {
  153. Rect item_area_rect { 0, 0, width() - m_vertical_scrollbar->width(), height() - m_horizontal_scrollbar->height() };
  154. painter.draw_rect(item_area_rect, Color::from_rgb(0x84351a));
  155. };
  156. }
  157. void GTextEditor::keydown_event(GKeyEvent& event)
  158. {
  159. if (!event.modifiers() && event.key() == KeyCode::Key_Up) {
  160. if (m_cursor.line() > 0) {
  161. int new_line = m_cursor.line() - 1;
  162. int new_column = min(m_cursor.column(), m_lines[new_line]->length());
  163. set_cursor(new_line, new_column);
  164. }
  165. return;
  166. }
  167. if (!event.modifiers() && event.key() == KeyCode::Key_Down) {
  168. if (m_cursor.line() < (m_lines.size() - 1)) {
  169. int new_line = m_cursor.line() + 1;
  170. int new_column = min(m_cursor.column(), m_lines[new_line]->length());
  171. set_cursor(new_line, new_column);
  172. }
  173. return;
  174. }
  175. if (!event.modifiers() && event.key() == KeyCode::Key_PageUp) {
  176. if (m_cursor.line() > 0) {
  177. int new_line = max(0, m_cursor.line() - visible_content_rect().height() / line_height());
  178. int new_column = min(m_cursor.column(), m_lines[new_line]->length());
  179. set_cursor(new_line, new_column);
  180. }
  181. return;
  182. }
  183. if (!event.modifiers() && event.key() == KeyCode::Key_PageDown) {
  184. if (m_cursor.line() < (m_lines.size() - 1)) {
  185. int new_line = min(line_count() - 1, m_cursor.line() + visible_content_rect().height() / line_height());
  186. int new_column = min(m_cursor.column(), m_lines[new_line]->length());
  187. set_cursor(new_line, new_column);
  188. }
  189. return;
  190. }
  191. if (!event.modifiers() && event.key() == KeyCode::Key_Left) {
  192. if (m_cursor.column() > 0) {
  193. int new_column = m_cursor.column() - 1;
  194. set_cursor(m_cursor.line(), new_column);
  195. }
  196. return;
  197. }
  198. if (!event.modifiers() && event.key() == KeyCode::Key_Right) {
  199. if (m_cursor.column() < current_line().length()) {
  200. int new_column = m_cursor.column() + 1;
  201. set_cursor(m_cursor.line(), new_column);
  202. }
  203. return;
  204. }
  205. if (!event.modifiers() && event.key() == KeyCode::Key_Home) {
  206. set_cursor(m_cursor.line(), 0);
  207. return;
  208. }
  209. if (!event.modifiers() && event.key() == KeyCode::Key_End) {
  210. set_cursor(m_cursor.line(), current_line().length());
  211. return;
  212. }
  213. if (event.ctrl() && event.key() == KeyCode::Key_Home) {
  214. set_cursor(0, 0);
  215. return;
  216. }
  217. if (event.ctrl() && event.key() == KeyCode::Key_End) {
  218. set_cursor(line_count() - 1, m_lines[line_count() - 1]->length());
  219. return;
  220. }
  221. if (!event.modifiers() && event.key() == KeyCode::Key_Backspace) {
  222. if (m_cursor.column() > 0) {
  223. // Backspace within line
  224. current_line().remove(m_cursor.column() - 1);
  225. update_scrollbar_ranges();
  226. set_cursor(m_cursor.line(), m_cursor.column() - 1);
  227. return;
  228. }
  229. if (m_cursor.column() == 0 && m_cursor.line() != 0) {
  230. // Backspace at column 0; merge with previous line
  231. auto& previous_line = *m_lines[m_cursor.line() - 1];
  232. int previous_length = previous_line.length();
  233. previous_line.append(current_line().characters(), current_line().length());
  234. m_lines.remove(m_cursor.line());
  235. update_scrollbar_ranges();
  236. update();
  237. set_cursor(m_cursor.line() - 1, previous_length);
  238. return;
  239. }
  240. return;
  241. }
  242. if (!event.modifiers() && event.key() == KeyCode::Key_Delete) {
  243. if (m_cursor.column() < current_line().length()) {
  244. // Delete within line
  245. current_line().remove(m_cursor.column());
  246. update_scrollbar_ranges();
  247. update_cursor();
  248. return;
  249. }
  250. if (m_cursor.column() == (current_line().length() + 1) && m_cursor.line() != line_count() - 1) {
  251. // Delete at end of line; merge with next line
  252. auto& next_line = *m_lines[m_cursor.line() + 1];
  253. int previous_length = current_line().length();
  254. current_line().append(next_line.characters(), next_line.length());
  255. m_lines.remove(m_cursor.line() + 1);
  256. update_scrollbar_ranges();
  257. update();
  258. set_cursor(m_cursor.line(), previous_length);
  259. return;
  260. }
  261. return;
  262. }
  263. if (!event.text().is_empty())
  264. insert_at_cursor(event.text()[0]);
  265. return GWidget::keydown_event(event);
  266. }
  267. void GTextEditor::insert_at_cursor(char ch)
  268. {
  269. bool at_head = m_cursor.column() == 0;
  270. bool at_tail = m_cursor.column() == current_line().length();
  271. if (ch == '\n') {
  272. if (at_tail || at_head) {
  273. m_lines.insert(m_cursor.line() + (at_tail ? 1 : 0), make<Line>());
  274. update_scrollbar_ranges();
  275. update();
  276. set_cursor(m_cursor.line() + 1, 0);
  277. return;
  278. }
  279. auto new_line = make<Line>();
  280. new_line->append(current_line().characters() + m_cursor.column(), current_line().length() - m_cursor.column());
  281. current_line().truncate(m_cursor.column());
  282. m_lines.insert(m_cursor.line() + 1, move(new_line));
  283. update_scrollbar_ranges();
  284. update();
  285. set_cursor(m_cursor.line() + 1, 0);
  286. return;
  287. }
  288. current_line().insert(m_cursor.column(), ch);
  289. update_scrollbar_ranges();
  290. set_cursor(m_cursor.line(), m_cursor.column() + 1);
  291. update_cursor();
  292. }
  293. Rect GTextEditor::visible_content_rect() const
  294. {
  295. return {
  296. m_horizontal_scrollbar->value(),
  297. m_vertical_scrollbar->value(),
  298. width() - m_vertical_scrollbar->width() - padding() * 2 - ruler_width(),
  299. height() - m_horizontal_scrollbar->height() - padding() * 2
  300. };
  301. }
  302. Rect GTextEditor::cursor_content_rect() const
  303. {
  304. if (!m_cursor.is_valid())
  305. return { };
  306. ASSERT(!m_lines.is_empty());
  307. ASSERT(m_cursor.column() <= (current_line().length() + 1));
  308. return { m_cursor.column() * glyph_width(), m_cursor.line() * line_height(), 1, line_height() };
  309. }
  310. Rect GTextEditor::line_widget_rect(int line_index) const
  311. {
  312. ASSERT(m_horizontal_scrollbar);
  313. ASSERT(m_vertical_scrollbar);
  314. auto rect = line_content_rect(line_index);
  315. rect.move_by(-(m_horizontal_scrollbar->value() - padding()), -(m_vertical_scrollbar->value() - padding()));
  316. rect.set_width(rect.width() + 1); // Add 1 pixel for when the cursor is on the end.
  317. rect.intersect(this->rect());
  318. // This feels rather hackish, but extend the rect to the edge of the content view:
  319. rect.set_right(m_vertical_scrollbar->relative_rect().left() - 1);
  320. return rect;
  321. }
  322. void GTextEditor::scroll_cursor_into_view()
  323. {
  324. auto visible_content_rect = this->visible_content_rect();
  325. auto rect = cursor_content_rect();
  326. if (visible_content_rect.is_empty())
  327. return;
  328. if (visible_content_rect.contains(rect))
  329. return;
  330. if (rect.top() < visible_content_rect.top())
  331. m_vertical_scrollbar->set_value(rect.top());
  332. else if (rect.bottom() > visible_content_rect.bottom())
  333. m_vertical_scrollbar->set_value(rect.bottom() - visible_content_rect.height());
  334. if (rect.left() < visible_content_rect.left())
  335. m_horizontal_scrollbar->set_value(rect.left());
  336. else if (rect.right() > visible_content_rect.right())
  337. m_horizontal_scrollbar->set_value(rect.right() - visible_content_rect.width());
  338. update();
  339. }
  340. Rect GTextEditor::line_content_rect(int line_index) const
  341. {
  342. return {
  343. 0,
  344. line_index * line_height(),
  345. content_width(),
  346. line_height()
  347. };
  348. }
  349. void GTextEditor::update_cursor()
  350. {
  351. update(line_widget_rect(m_cursor.line()));
  352. }
  353. void GTextEditor::set_cursor(int line, int column)
  354. {
  355. set_cursor({ line, column });
  356. }
  357. void GTextEditor::set_cursor(const GTextPosition& position)
  358. {
  359. if (m_cursor == position)
  360. return;
  361. auto old_cursor_line_rect = line_widget_rect(m_cursor.line());
  362. m_cursor = position;
  363. m_cursor_state = true;
  364. scroll_cursor_into_view();
  365. update(old_cursor_line_rect);
  366. update_cursor();
  367. if (on_cursor_change)
  368. on_cursor_change(*this);
  369. }
  370. void GTextEditor::focusin_event(GEvent&)
  371. {
  372. update_cursor();
  373. start_timer(500);
  374. }
  375. void GTextEditor::focusout_event(GEvent&)
  376. {
  377. stop_timer();
  378. }
  379. void GTextEditor::timer_event(GTimerEvent&)
  380. {
  381. m_cursor_state = !m_cursor_state;
  382. if (is_focused())
  383. update_cursor();
  384. }
  385. GTextEditor::Line::Line()
  386. {
  387. m_text.append(0);
  388. }
  389. void GTextEditor::Line::set_text(const String& text)
  390. {
  391. if (text.length() == length() && !memcmp(text.characters(), characters(), length()))
  392. return;
  393. m_text.resize(text.length() + 1);
  394. memcpy(m_text.data(), text.characters(), text.length() + 1);
  395. }
  396. int GTextEditor::Line::width(const Font& font) const
  397. {
  398. return font.glyph_width('x') * length();
  399. }
  400. void GTextEditor::Line::append(const char* characters, int length)
  401. {
  402. int old_length = m_text.size() - 1;
  403. m_text.resize(m_text.size() + length);
  404. memcpy(m_text.data() + old_length, characters, length);
  405. m_text.last() = 0;
  406. }
  407. void GTextEditor::Line::append(char ch)
  408. {
  409. insert(length(), ch);
  410. }
  411. void GTextEditor::Line::prepend(char ch)
  412. {
  413. insert(0, ch);
  414. }
  415. void GTextEditor::Line::insert(int index, char ch)
  416. {
  417. if (index == length()) {
  418. m_text.last() = ch;
  419. m_text.append(0);
  420. } else {
  421. m_text.insert(index, move(ch));
  422. }
  423. }
  424. void GTextEditor::Line::remove(int index)
  425. {
  426. if (index == length()) {
  427. m_text.take_last();
  428. m_text.last() = 0;
  429. } else {
  430. m_text.remove(index);
  431. }
  432. }
  433. void GTextEditor::Line::truncate(int length)
  434. {
  435. m_text.resize(length + 1);
  436. m_text.last() = 0;
  437. }
  438. bool GTextEditor::write_to_file(const String& path)
  439. {
  440. int fd = open(path.characters(), O_WRONLY | O_CREAT, 0666);
  441. if (fd < 0) {
  442. perror("open");
  443. return false;
  444. }
  445. for (int i = 0; i < m_lines.size(); ++i) {
  446. auto& line = *m_lines[i];
  447. if (line.length()) {
  448. ssize_t nwritten = write(fd, line.characters(), line.length());
  449. if (nwritten < 0) {
  450. perror("write");
  451. close(fd);
  452. return false;
  453. }
  454. }
  455. if (i != m_lines.size() - 1) {
  456. char ch = '\n';
  457. ssize_t nwritten = write(fd, &ch, 1);
  458. if (nwritten != 1) {
  459. perror("write");
  460. close(fd);
  461. return false;
  462. }
  463. }
  464. }
  465. close(fd);
  466. return true;
  467. }