diff --git a/Applications/TextEditor/.gitignore b/Applications/TextEditor/.gitignore new file mode 100644 index 0000000000..cf1007cf63 --- /dev/null +++ b/Applications/TextEditor/.gitignore @@ -0,0 +1,3 @@ +*.o +*.d +TextEditor diff --git a/Applications/TextEditor/Makefile b/Applications/TextEditor/Makefile new file mode 100644 index 0000000000..f017bcb9dd --- /dev/null +++ b/Applications/TextEditor/Makefile @@ -0,0 +1,32 @@ +OBJS = \ + main.o + +APP = TextEditor + +STANDARD_FLAGS = -std=c++17 +WARNING_FLAGS = -Wextra -Wall -Wundef -Wcast-qual -Wwrite-strings -Wimplicit-fallthrough +FLAVOR_FLAGS = -fno-exceptions -fno-rtti +OPTIMIZATION_FLAGS = -Os +INCLUDE_FLAGS = -I../.. -I. -I../../LibC + +DEFINES = -DSERENITY -DSANITIZE_PTRS -DUSERLAND + +CXXFLAGS = -MMD -MP $(WARNING_FLAGS) $(OPTIMIZATION_FLAGS) $(FLAVOR_FLAGS) $(STANDARD_FLAGS) $(INCLUDE_FLAGS) $(DEFINES) +CXX = i686-pc-serenity-g++ +LD = i686-pc-serenity-ld +AR = i686-pc-serenity-ar +LDFLAGS = -L../../LibC -L../../LibGUI + +all: $(APP) + +$(APP): $(OBJS) + $(LD) -o $(APP) $(LDFLAGS) $(OBJS) -lgui -lc + +.cpp.o: + @echo "CXX $<"; $(CXX) $(CXXFLAGS) -o $@ -c $< + +-include $(OBJS:%.o=%.d) + +clean: + @echo "CLEAN"; rm -f $(APPS) $(OBJS) *.d + diff --git a/Applications/TextEditor/main.cpp b/Applications/TextEditor/main.cpp new file mode 100644 index 0000000000..10845ac060 --- /dev/null +++ b/Applications/TextEditor/main.cpp @@ -0,0 +1,86 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +int main(int argc, char** argv) +{ + GApplication app(argc, argv); + + auto* widget = new GWidget; + widget->set_fill_with_background_color(false); + widget->set_layout(make(Orientation::Vertical)); + + auto* toolbar = new GToolBar(widget); + auto* text_editor = new GTextEditor(widget); + auto* statusbar = new GStatusBar(widget); + + { + StringBuilder builder; + int fd = open("/home/anon/ReadMe.md", O_RDONLY); + if (fd < 0) { + perror("open"); + return 1; + } + for (;;) { + char buffer[BUFSIZ]; + ssize_t nread = read(fd, buffer, sizeof(buffer)); + if (nread < 0) { + perror("read"); + return 1; + } + if (nread == 0) + break; + builder.append(buffer, nread); + } + + int rc = close(fd); + if (rc < 0) { + perror("close"); + return 1; + } + + text_editor->set_text(builder.to_string()); + } + + auto menubar = make(); + auto app_menu = make("TextEditor"); + app_menu->add_action(GAction::create("Quit", { Mod_Alt, Key_F4 }, [] (const GAction&) { + GApplication::the().quit(0); + return; + })); + menubar->add_menu(move(app_menu)); + + auto file_menu = make("File"); + menubar->add_menu(move(file_menu)); + + auto edit_menu = make("Edit"); + menubar->add_menu(move(edit_menu)); + + auto help_menu = make("Help"); + help_menu->add_action(GAction::create("About", [] (const GAction&) { + dbgprintf("FIXME: Implement Help/About\n"); + })); + menubar->add_menu(move(help_menu)); + + app.set_menubar(move(menubar)); + + auto* window = new GWindow; + window->set_title("TextEditor"); + window->set_rect(20, 200, 640, 400); + window->set_main_widget(widget); + window->set_should_exit_app_on_close(true); + window->show(); + + return app.exec(); +} diff --git a/Kernel/init.cpp b/Kernel/init.cpp index a5a1e3350c..b0d38a9c47 100644 --- a/Kernel/init.cpp +++ b/Kernel/init.cpp @@ -27,7 +27,8 @@ #define SPAWN_LAUNCHER //#define SPAWN_GUITEST2 //#define SPAWN_FILE_MANAGER -#define SPAWN_PROCESS_MANAGER +//#define SPAWN_PROCESS_MANAGER +#define SPAWN_TEXT_EDITOR //#define SPAWN_FONTEDITOR //#define SPAWN_MULTIPLE_SHELLS //#define STRESS_TEST_SPAWNING @@ -102,6 +103,9 @@ VFS* vfs; #ifdef SPAWN_PROCESS_MANAGER Process::create_user_process("/bin/ProcessManager", (uid_t)100, (gid_t)100, (pid_t)0, error, { }, { }, tty0); #endif +#ifdef SPAWN_TEXT_EDITOR + Process::create_user_process("/bin/TextEditor", (uid_t)100, (gid_t)100, (pid_t)0, error, { }, { }, tty0); +#endif #ifdef SPAWN_FONTEDITOR Process::create_user_process("/bin/FontEditor", (uid_t)100, (gid_t)100, (pid_t)0, error, { }, move(environment), tty0); #endif diff --git a/Kernel/makeall.sh b/Kernel/makeall.sh index b6c85c756e..2448f50a1e 100755 --- a/Kernel/makeall.sh +++ b/Kernel/makeall.sh @@ -28,6 +28,8 @@ $make_cmd -C ../Applications/FileManager clean && \ $make_cmd -C ../Applications/FileManager && \ $make_cmd -C ../Applications/ProcessManager clean && \ $make_cmd -C ../Applications/ProcessManager && \ +$make_cmd -C ../Applications/TextEditor clean && \ +$make_cmd -C ../Applications/TextEditor && \ $make_cmd -C ../Applications/About clean && \ $make_cmd -C ../Applications/About && \ $make_cmd clean &&\ diff --git a/Kernel/sync.sh b/Kernel/sync.sh index 25627ed93e..00f5876218 100755 --- a/Kernel/sync.sh +++ b/Kernel/sync.sh @@ -35,6 +35,7 @@ cp -vR ../Base/* mnt/ cp -vR ../Root/* mnt/ mkdir mnt/home/anon mkdir mnt/home/nona +cp ../ReadMe.md mnt/home/anon/ chown -vR 100:100 mnt/home/anon chown -vR 200:200 mnt/home/nona cp -v ../Userland/sh mnt/bin/sh @@ -80,6 +81,7 @@ cp -v ../Applications/Launcher/Launcher mnt/bin/Launcher cp -v ../Applications/FileManager/FileManager mnt/bin/FileManager cp -v ../Applications/ProcessManager/ProcessManager mnt/bin/ProcessManager cp -v ../Applications/About/About mnt/bin/About +cp -v ../Applications/TextEditor/TextEditor mnt/bin/TextEditor cp -v ../WindowServer/WindowServer mnt/bin/WindowServer cp -v kernel.map mnt/ sh sync-local.sh diff --git a/LibGUI/GTextEditor.cpp b/LibGUI/GTextEditor.cpp new file mode 100644 index 0000000000..99f0999d5c --- /dev/null +++ b/LibGUI/GTextEditor.cpp @@ -0,0 +1,226 @@ +#include +#include +#include +#include +#include + +GTextEditor::GTextEditor(GWidget* parent) + : GWidget(parent) +{ + set_font(GFontDatabase::the().get_by_name("Liza Thin")); + + set_fill_with_background_color(false); + + m_vertical_scrollbar = new GScrollBar(Orientation::Vertical, this); + m_vertical_scrollbar->set_step(4); + m_vertical_scrollbar->on_change = [this] (int) { + update(); + }; + + m_horizontal_scrollbar = new GScrollBar(Orientation::Horizontal, this); + m_horizontal_scrollbar->set_step(4); + m_horizontal_scrollbar->set_big_step(30); + m_horizontal_scrollbar->on_change = [this] (int) { + update(); + }; +} + +GTextEditor::~GTextEditor() +{ +} + +void GTextEditor::set_text(const String& text) +{ + m_lines.clear(); + int start_of_current_line = 0; + + auto add_line = [&] (int current_position) { + int line_length = current_position - start_of_current_line; + Line line; + if (line_length) + line.set_text(text.substring(start_of_current_line, current_position - start_of_current_line)); + m_lines.append(move(line)); + start_of_current_line = current_position + 1; + }; + int i = 0; + for (i = 0; i < text.length(); ++i) { + if (text[i] == '\n') + add_line(i); + } + add_line(i); + m_cursor = GTextPosition(0, 0); + update(); +} + +void GTextEditor::resize_event(GResizeEvent& event) +{ + update_scrollbar_ranges(); + 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()); + 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()); +} + +void GTextEditor::update_scrollbar_ranges() +{ + int available_height = height() - m_horizontal_scrollbar->height(); + int excess_height = max(0, (line_count() * font().glyph_height()) - available_height); + m_vertical_scrollbar->set_range(0, excess_height); + + int available_width = width() - m_vertical_scrollbar->width(); + int excess_width = max(0, content_width() - available_width); + m_horizontal_scrollbar->set_range(0, excess_width); + + m_vertical_scrollbar->set_big_step(visible_content_rect().height()); +} + +int GTextEditor::content_width() const +{ + int max_width = 0; + for (auto& line : m_lines) + max_width = max(line.width(font()), max_width); + return max_width; +} + +void GTextEditor::mousedown_event(GMouseEvent& event) +{ + (void)event; +} + +void GTextEditor::paint_event(GPaintEvent& event) +{ + Painter painter(*this); + painter.set_clip_rect(event.rect()); + painter.fill_rect(event.rect(), Color::White); + painter.translate(-m_horizontal_scrollbar->value(), -m_vertical_scrollbar->value()); + painter.translate(padding(), padding()); + int exposed_width = max(content_width(), width()); + + for (int i = 0; i < line_count(); ++i) { + auto& line = m_lines[i]; + auto line_rect = line_content_rect(i); + painter.draw_text(line_rect, line.text(), TextAlignment::TopLeft, Color::Black); + } + + painter.fill_rect(cursor_content_rect(), Color::Red); + + painter.translate(-padding(), -padding()); + painter.translate(m_horizontal_scrollbar->value(), m_vertical_scrollbar->value()); + 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); + + if (is_focused()) { + Rect item_area_rect { 0, 0, width() - m_vertical_scrollbar->width(), height() - m_horizontal_scrollbar->height() }; + painter.draw_rect(item_area_rect, Color::from_rgb(0x84351a)); + }; +} + +void GTextEditor::keydown_event(GKeyEvent& event) +{ + if (event.key() == KeyCode::Key_Up) { + if (m_cursor.line() > 0) { + update_cursor(); + m_cursor.set_line(m_cursor.line() - 1); + m_cursor.set_column(min(m_cursor.column(), m_lines[m_cursor.line()].length())); + update_cursor(); + } + return; + } + if (event.key() == KeyCode::Key_Down) { + if (m_cursor.line() < (m_lines.size() - 1)) { + update_cursor(); + m_cursor.set_line(m_cursor.line() + 1); + m_cursor.set_column(min(m_cursor.column(), m_lines[m_cursor.line()].length())); + update_cursor(); + } + return; + } + if (event.key() == KeyCode::Key_Left) { + if (m_cursor.column() > 0) { + update_cursor(); + m_cursor.set_column(m_cursor.column() - 1); + update_cursor(); + } + return; + } + if (event.key() == KeyCode::Key_Right) { + if (m_cursor.column() < (m_lines[m_cursor.line()].length())) { + update_cursor(); + m_cursor.set_column(m_cursor.column() + 1); + update_cursor(); + } + return; + } + return GWidget::keydown_event(event); +} + +Rect GTextEditor::visible_content_rect() const +{ + return { + m_horizontal_scrollbar->value(), + m_vertical_scrollbar->value(), + width() - m_vertical_scrollbar->width() - padding() * 2, + height() - m_horizontal_scrollbar->height() - padding() * 2 + }; +} + +Rect GTextEditor::cursor_content_rect() const +{ + ASSERT(m_cursor.is_valid()); + ASSERT(!m_lines.is_empty()); + auto& line = m_lines[m_cursor.line()]; + ASSERT(m_cursor.column() <= (line.text().length() + 1)); + int x = 0; + for (int i = 0; i < m_cursor.column(); ++i) + x += font().glyph_width(line.text()[i]); + return { x, m_cursor.line() * font().glyph_height(), 1, font().glyph_height() }; +} + +void GTextEditor::scroll_into_view(const GTextPosition& position, Orientation orientation) +{ + auto visible_content_rect = this->visible_content_rect(); + auto rect = line_content_rect(position.line()); + + if (visible_content_rect.contains(rect)) + return; + + if (orientation == Orientation::Vertical) { + if (rect.top() < visible_content_rect.top()) + m_vertical_scrollbar->set_value(rect.top()); + else if (rect.bottom() > visible_content_rect.bottom()) + m_vertical_scrollbar->set_value(rect.bottom() - visible_content_rect.height()); + } else { + if (rect.left() < visible_content_rect.left()) + m_horizontal_scrollbar->set_value(rect.left()); + else if (rect.right() > visible_content_rect.right()) + m_horizontal_scrollbar->set_value(rect.right() - visible_content_rect.width()); + } +} + +Rect GTextEditor::line_content_rect(int line_index) const +{ + auto& line = m_lines[line_index]; + return { + 0, + line_index * font().glyph_height(), + line.width(font()), + font().glyph_height() + }; +} + +void GTextEditor::update_cursor() +{ + update(); +} + +void GTextEditor::Line::set_text(const String& text) +{ + if (text == m_text) + return; + m_text = text; + m_cached_width = -1; +} + +int GTextEditor::Line::width(const Font& font) const +{ + if (m_cached_width < 0) + m_cached_width = font.width(m_text); + return m_cached_width; +} diff --git a/LibGUI/GTextEditor.h b/LibGUI/GTextEditor.h new file mode 100644 index 0000000000..ed0b8facde --- /dev/null +++ b/LibGUI/GTextEditor.h @@ -0,0 +1,77 @@ +#pragma once + +#include +#include +#include + +class GScrollBar; + +class GTextPosition { +public: + GTextPosition() { } + GTextPosition(int line, int column) + : m_line(line) + , m_column(column) + { + } + + bool is_valid() const { return m_line >= 0 && m_column >= 0; } + + int line() const { return m_line; } + int column() const { return m_column; } + + void set_line(int line) { m_line = line; } + void set_column(int column) { m_column = column; } + +private: + int m_line { -1 }; + int m_column { -1 }; +}; + +class GTextEditor : public GWidget { +public: + explicit GTextEditor(GWidget* parent); + virtual ~GTextEditor() override; + + void set_text(const String&); + + int content_width() const; + + Rect visible_content_rect() const; + void scroll_into_view(const GTextPosition&, Orientation); + + int line_count() const { return m_lines.size(); } + + int padding() const { return 2; } + +private: + virtual void paint_event(GPaintEvent&) override; + virtual void resize_event(GResizeEvent&) override; + virtual void mousedown_event(GMouseEvent&) override; + virtual void keydown_event(GKeyEvent&) override; + virtual bool accepts_focus() const override { return true; } + + void update_scrollbar_ranges(); + Rect line_content_rect(int item_index) const; + Rect cursor_content_rect() const; + void update_cursor(); + + GScrollBar* m_vertical_scrollbar { nullptr }; + GScrollBar* m_horizontal_scrollbar { nullptr }; + + class Line { + public: + Line() { } + + String text() const { return m_text; } + int length() const { return m_text.length(); } + int width(const Font&) const; + void set_text(const String&); + + private: + String m_text; + mutable int m_cached_width { -1 }; + }; + Vector m_lines; + GTextPosition m_cursor; +}; diff --git a/LibGUI/Makefile b/LibGUI/Makefile index 29e8b33006..9b1fd840e4 100644 --- a/LibGUI/Makefile +++ b/LibGUI/Makefile @@ -32,6 +32,7 @@ LIBGUI_OBJS = \ GTableModel.o \ GVariant.o \ GShortcut.o \ + GTextEditor.o \ GWindow.o OBJS = $(SHAREDGRAPHICS_OBJS) $(LIBGUI_OBJS)