GTextEditor.cpp 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486
  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();
  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(), -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. void GTextEditor::paint_event(GPaintEvent& event)
  95. {
  96. Painter painter(*this);
  97. painter.set_clip_rect(event.rect());
  98. painter.fill_rect(event.rect(), Color::White);
  99. painter.translate(-m_horizontal_scrollbar->value(), -m_vertical_scrollbar->value());
  100. painter.translate(padding(), padding());
  101. int exposed_width = max(content_width(), width());
  102. int first_visible_line = text_position_at(event.rect().top_left()).line();
  103. int last_visible_line = text_position_at(event.rect().bottom_right()).line();
  104. for (int i = first_visible_line; i <= last_visible_line; ++i) {
  105. auto& line = *m_lines[i];
  106. auto line_rect = line_content_rect(i);
  107. line_rect.set_width(exposed_width);
  108. if (i == m_cursor.line() && is_focused())
  109. painter.fill_rect(line_rect, Color(230, 230, 230));
  110. painter.draw_text(line_rect, line.characters(), line.length(), TextAlignment::CenterLeft, Color::Black);
  111. }
  112. if (is_focused() && m_cursor_state)
  113. painter.fill_rect(cursor_content_rect(), Color::Red);
  114. painter.translate(-padding(), -padding());
  115. painter.translate(m_horizontal_scrollbar->value(), m_vertical_scrollbar->value());
  116. 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);
  117. if (is_focused()) {
  118. Rect item_area_rect { 0, 0, width() - m_vertical_scrollbar->width(), height() - m_horizontal_scrollbar->height() };
  119. painter.draw_rect(item_area_rect, Color::from_rgb(0x84351a));
  120. };
  121. }
  122. void GTextEditor::keydown_event(GKeyEvent& event)
  123. {
  124. if (!event.modifiers() && event.key() == KeyCode::Key_Up) {
  125. if (m_cursor.line() > 0) {
  126. int new_line = m_cursor.line() - 1;
  127. int new_column = min(m_cursor.column(), m_lines[new_line]->length());
  128. set_cursor(new_line, new_column);
  129. }
  130. return;
  131. }
  132. if (!event.modifiers() && event.key() == KeyCode::Key_Down) {
  133. if (m_cursor.line() < (m_lines.size() - 1)) {
  134. int new_line = m_cursor.line() + 1;
  135. int new_column = min(m_cursor.column(), m_lines[new_line]->length());
  136. set_cursor(new_line, new_column);
  137. }
  138. return;
  139. }
  140. if (!event.modifiers() && event.key() == KeyCode::Key_PageUp) {
  141. if (m_cursor.line() > 0) {
  142. int new_line = max(0, m_cursor.line() - visible_content_rect().height() / line_height());
  143. int new_column = min(m_cursor.column(), m_lines[new_line]->length());
  144. set_cursor(new_line, new_column);
  145. }
  146. return;
  147. }
  148. if (!event.modifiers() && event.key() == KeyCode::Key_PageDown) {
  149. if (m_cursor.line() < (m_lines.size() - 1)) {
  150. int new_line = min(line_count() - 1, m_cursor.line() + visible_content_rect().height() / line_height());
  151. int new_column = min(m_cursor.column(), m_lines[new_line]->length());
  152. set_cursor(new_line, new_column);
  153. }
  154. return;
  155. }
  156. if (!event.modifiers() && event.key() == KeyCode::Key_Left) {
  157. if (m_cursor.column() > 0) {
  158. int new_column = m_cursor.column() - 1;
  159. set_cursor(m_cursor.line(), new_column);
  160. }
  161. return;
  162. }
  163. if (!event.modifiers() && event.key() == KeyCode::Key_Right) {
  164. if (m_cursor.column() < current_line().length()) {
  165. int new_column = m_cursor.column() + 1;
  166. set_cursor(m_cursor.line(), new_column);
  167. }
  168. return;
  169. }
  170. if (!event.modifiers() && event.key() == KeyCode::Key_Home) {
  171. set_cursor(m_cursor.line(), 0);
  172. return;
  173. }
  174. if (!event.modifiers() && event.key() == KeyCode::Key_End) {
  175. set_cursor(m_cursor.line(), current_line().length());
  176. return;
  177. }
  178. if (event.ctrl() && event.key() == KeyCode::Key_Home) {
  179. set_cursor(0, 0);
  180. return;
  181. }
  182. if (event.ctrl() && event.key() == KeyCode::Key_End) {
  183. set_cursor(line_count() - 1, m_lines[line_count() - 1]->length());
  184. return;
  185. }
  186. if (!event.modifiers() && event.key() == KeyCode::Key_Backspace) {
  187. if (m_cursor.column() > 0) {
  188. // Backspace within line
  189. current_line().remove(m_cursor.column() - 1);
  190. update_scrollbar_ranges();
  191. set_cursor(m_cursor.line(), m_cursor.column() - 1);
  192. return;
  193. }
  194. if (m_cursor.column() == 0 && m_cursor.line() != 0) {
  195. // Backspace at column 0; merge with previous line
  196. auto& previous_line = *m_lines[m_cursor.line() - 1];
  197. int previous_length = previous_line.length();
  198. previous_line.append(current_line().characters(), current_line().length());
  199. m_lines.remove(m_cursor.line());
  200. update_scrollbar_ranges();
  201. update();
  202. set_cursor(m_cursor.line() - 1, previous_length);
  203. return;
  204. }
  205. return;
  206. }
  207. if (!event.modifiers() && event.key() == KeyCode::Key_Delete) {
  208. if (m_cursor.column() < current_line().length()) {
  209. // Delete within line
  210. current_line().remove(m_cursor.column());
  211. update_scrollbar_ranges();
  212. update_cursor();
  213. return;
  214. }
  215. if (m_cursor.column() == (current_line().length() + 1) && m_cursor.line() != line_count() - 1) {
  216. // Delete at end of line; merge with next line
  217. auto& next_line = *m_lines[m_cursor.line() + 1];
  218. int previous_length = current_line().length();
  219. current_line().append(next_line.characters(), next_line.length());
  220. m_lines.remove(m_cursor.line() + 1);
  221. update_scrollbar_ranges();
  222. update();
  223. set_cursor(m_cursor.line(), previous_length);
  224. return;
  225. }
  226. return;
  227. }
  228. if (!event.text().is_empty())
  229. insert_at_cursor(event.text()[0]);
  230. return GWidget::keydown_event(event);
  231. }
  232. void GTextEditor::insert_at_cursor(char ch)
  233. {
  234. bool at_head = m_cursor.column() == 0;
  235. bool at_tail = m_cursor.column() == current_line().length();
  236. if (ch == '\n') {
  237. if (at_tail || at_head) {
  238. m_lines.insert(m_cursor.line() + (at_tail ? 1 : 0), make<Line>());
  239. update_scrollbar_ranges();
  240. update();
  241. set_cursor(m_cursor.line() + 1, 0);
  242. return;
  243. }
  244. auto new_line = make<Line>();
  245. new_line->append(current_line().characters() + m_cursor.column(), current_line().length() - m_cursor.column());
  246. current_line().truncate(m_cursor.column());
  247. m_lines.insert(m_cursor.line() + 1, move(new_line));
  248. update_scrollbar_ranges();
  249. update();
  250. set_cursor(m_cursor.line() + 1, 0);
  251. return;
  252. }
  253. current_line().insert(m_cursor.column(), ch);
  254. set_cursor(m_cursor.line(), m_cursor.column() + 1);
  255. update_cursor();
  256. }
  257. Rect GTextEditor::visible_content_rect() const
  258. {
  259. return {
  260. m_horizontal_scrollbar->value(),
  261. m_vertical_scrollbar->value(),
  262. width() - m_vertical_scrollbar->width() - padding() * 2,
  263. height() - m_horizontal_scrollbar->height() - padding() * 2
  264. };
  265. }
  266. Rect GTextEditor::cursor_content_rect() const
  267. {
  268. if (!m_cursor.is_valid())
  269. return { };
  270. ASSERT(!m_lines.is_empty());
  271. ASSERT(m_cursor.column() <= (current_line().length() + 1));
  272. return { m_cursor.column() * glyph_width(), m_cursor.line() * line_height(), 1, line_height() };
  273. }
  274. Rect GTextEditor::line_widget_rect(int line_index) const
  275. {
  276. ASSERT(m_horizontal_scrollbar);
  277. ASSERT(m_vertical_scrollbar);
  278. auto rect = line_content_rect(line_index);
  279. rect.move_by(-(m_horizontal_scrollbar->value() - padding()), -(m_vertical_scrollbar->value() - padding()));
  280. rect.set_width(rect.width() + 1); // Add 1 pixel for when the cursor is on the end.
  281. rect.intersect(this->rect());
  282. // This feels rather hackish, but extend the rect to the edge of the content view:
  283. rect.set_right(m_vertical_scrollbar->relative_rect().left() - 1);
  284. return rect;
  285. }
  286. void GTextEditor::scroll_cursor_into_view()
  287. {
  288. auto visible_content_rect = this->visible_content_rect();
  289. auto rect = cursor_content_rect();
  290. if (visible_content_rect.is_empty())
  291. return;
  292. if (visible_content_rect.contains(rect))
  293. return;
  294. if (rect.top() < visible_content_rect.top())
  295. m_vertical_scrollbar->set_value(rect.top());
  296. else if (rect.bottom() > visible_content_rect.bottom())
  297. m_vertical_scrollbar->set_value(rect.bottom() - visible_content_rect.height());
  298. if (rect.left() < visible_content_rect.left())
  299. m_horizontal_scrollbar->set_value(rect.left());
  300. else if (rect.right() > visible_content_rect.right())
  301. m_horizontal_scrollbar->set_value(rect.right() - visible_content_rect.width());
  302. update();
  303. }
  304. Rect GTextEditor::line_content_rect(int line_index) const
  305. {
  306. return {
  307. 0,
  308. line_index * line_height(),
  309. content_width(),
  310. line_height()
  311. };
  312. }
  313. void GTextEditor::update_cursor()
  314. {
  315. update(line_widget_rect(m_cursor.line()));
  316. }
  317. void GTextEditor::set_cursor(int line, int column)
  318. {
  319. set_cursor({ line, column });
  320. }
  321. void GTextEditor::set_cursor(const GTextPosition& position)
  322. {
  323. if (m_cursor == position)
  324. return;
  325. auto old_cursor_line_rect = line_widget_rect(m_cursor.line());
  326. m_cursor = position;
  327. m_cursor_state = true;
  328. scroll_cursor_into_view();
  329. update(old_cursor_line_rect);
  330. update_cursor();
  331. if (on_cursor_change)
  332. on_cursor_change(*this);
  333. }
  334. void GTextEditor::focusin_event(GEvent&)
  335. {
  336. update_cursor();
  337. start_timer(500);
  338. }
  339. void GTextEditor::focusout_event(GEvent&)
  340. {
  341. stop_timer();
  342. }
  343. void GTextEditor::timer_event(GTimerEvent&)
  344. {
  345. m_cursor_state = !m_cursor_state;
  346. if (is_focused())
  347. update_cursor();
  348. }
  349. GTextEditor::Line::Line()
  350. {
  351. m_text.append(0);
  352. }
  353. void GTextEditor::Line::set_text(const String& text)
  354. {
  355. if (text.length() == length() && !memcmp(text.characters(), characters(), length()))
  356. return;
  357. m_text.resize(text.length() + 1);
  358. memcpy(m_text.data(), text.characters(), text.length() + 1);
  359. }
  360. int GTextEditor::Line::width(const Font& font) const
  361. {
  362. return font.glyph_width('x') * length();
  363. }
  364. void GTextEditor::Line::append(const char* characters, int length)
  365. {
  366. int old_length = m_text.size() - 1;
  367. m_text.resize(m_text.size() + length);
  368. memcpy(m_text.data() + old_length, characters, length);
  369. m_text.last() = 0;
  370. }
  371. void GTextEditor::Line::append(char ch)
  372. {
  373. insert(length(), ch);
  374. }
  375. void GTextEditor::Line::prepend(char ch)
  376. {
  377. insert(0, ch);
  378. }
  379. void GTextEditor::Line::insert(int index, char ch)
  380. {
  381. if (index == length()) {
  382. m_text.last() = ch;
  383. m_text.append(0);
  384. } else {
  385. m_text.insert(index, move(ch));
  386. }
  387. }
  388. void GTextEditor::Line::remove(int index)
  389. {
  390. if (index == length()) {
  391. m_text.take_last();
  392. m_text.last() = 0;
  393. } else {
  394. m_text.remove(index);
  395. }
  396. }
  397. void GTextEditor::Line::truncate(int length)
  398. {
  399. m_text.resize(length + 1);
  400. m_text.last() = 0;
  401. }
  402. bool GTextEditor::write_to_file(const String& path)
  403. {
  404. int fd = open(path.characters(), O_WRONLY | O_CREAT, 0666);
  405. if (fd < 0) {
  406. perror("open");
  407. return false;
  408. }
  409. for (int i = 0; i < m_lines.size(); ++i) {
  410. auto& line = *m_lines[i];
  411. if (line.length()) {
  412. ssize_t nwritten = write(fd, line.characters(), line.length());
  413. if (nwritten < 0) {
  414. perror("write");
  415. close(fd);
  416. return false;
  417. }
  418. }
  419. if (i != m_lines.size() - 1) {
  420. char ch = '\n';
  421. ssize_t nwritten = write(fd, &ch, 1);
  422. if (nwritten != 1) {
  423. perror("write");
  424. close(fd);
  425. return false;
  426. }
  427. }
  428. }
  429. close(fd);
  430. return true;
  431. }