diff --git a/README.md b/README.md index 732c7db..f132999 100644 --- a/README.md +++ b/README.md @@ -8,4 +8,3 @@ edit code and text - blissfully ## development setup instructions * `pip install -r requirements.txt` * `git clone https://github.com/rexxt/tifer.git` - diff --git a/UI.json b/UI.json index 5000e7b..11baed9 100644 --- a/UI.json +++ b/UI.json @@ -1,6 +1,17 @@ { - "application_background": [17, 6, 34], - "interface_font": ["consolas", 14], - "status_bar_background": [180, 134, 255], - "status_bar_foreground": [64, 0, 168] + "application_background": [14, 17, 20], + "application_background_highlight": [54, 57, 60], + "interface_font": ["ibmplexsans, firasansregular, arial", 14], + "editor_font": ["firacoderegular, sourcecodepro, consolas", 14], + "status_bar_background": [33, 39, 46], + "status_bar_foreground": [149, 255, 0], + "tab_bar_background": [33, 39, 46], + "tab_bar_foreground": [66, 135, 245], + "tab_focus_background": [66, 135, 245], + "tab_focus_foreground": [4, 18, 41], + "toasts": { + "base_background": [33, 39, 46], + "info": [[66, 135, 245], [255, 255, 255]], + "error": [[242, 41, 55], [255, 255, 255]] + } } \ No newline at end of file diff --git a/bliss.py b/bliss.py index 33da96e..ee1123b 100644 --- a/bliss.py +++ b/bliss.py @@ -10,9 +10,12 @@ import pygame import tools import json -ui_config = json.load(open('UI.json')) +import time +import shlex -bliss_version = '0.1.1' +# define tabs +import tabs.welcome as tab_welcome +import tabs.editor as tab_editor if not os.path.isdir('tifer'): print('[red]tifer not installed, exiting[/red]') @@ -21,47 +24,191 @@ # from tifer.tifer import FileEditor -def main(): - pygame.init() - - interface_font = pygame.font.SysFont(ui_config['interface_font'][0], ui_config['interface_font'][1]) - interface_font_bold = pygame.font.SysFont(ui_config['interface_font'][0], ui_config['interface_font'][1], bold=True) - - project = '' - if len(command_line_arguments) > 0: - project = command_line_arguments.pop() - - pygame.display.set_caption("Bliss - " + ("no project" if project == '' else project)) - logo = pygame.image.load("icon.png") - pygame.display.set_icon(logo) - - screen = pygame.display.set_mode((640,360), pygame.RESIZABLE) - - keys = [] - running = True - while running: - screen.fill(ui_config['application_background']) - - - for event in pygame.event.get(): - #print(event) - if event.type == pygame.QUIT: - running = False - if event.type == pygame.VIDEORESIZE: - screen = pygame.display.set_mode((event.w, event.h), pygame.RESIZABLE) - if event.type == pygame.KEYDOWN: - keys.append(str(event.scancode)) - elif event.type == pygame.KEYUP: - keys.remove(str(event.scancode)) - - w, h = pygame.display.get_surface().get_size() - status_bar_pos = tools.Point((0, h-ui_config['interface_font'][1]*2)) - pygame.draw.rect(screen, ui_config['status_bar_background'], pygame.Rect(status_bar_pos[0], status_bar_pos[1], w, ui_config['interface_font'][1]*2)) - tools.write_to_screen(screen, interface_font_bold, 'Bliss ' + bliss_version, status_bar_pos + tools.Point((ui_config['interface_font'][1]/2, ui_config['interface_font'][1]/2)), (True, ui_config['status_bar_foreground'])) - if len(keys) > 0: - tools.write_to_screen(screen, interface_font_bold, ' '.join(keys), status_bar_pos + tools.Point((ui_config['interface_font'][1]/2 + 100, ui_config['interface_font'][1]/2)), (True, ui_config['status_bar_foreground'])) - - pygame.display.flip() - -if __name__=="__main__": - main() \ No newline at end of file +pygame.init() + +class App: + VERSION = '0.1.1' + + def __init__(self): + self.ui_config = json.load(open('UI.json')) + + self.interface_font = pygame.font.SysFont(self.ui_config['interface_font'][0], self.ui_config['interface_font'][1]) + self.interface_font_bold = pygame.font.SysFont(self.ui_config['interface_font'][0], self.ui_config['interface_font'][1], bold=True) + self.editor_font = pygame.font.SysFont(self.ui_config['editor_font'][0], self.ui_config['editor_font'][1]) + self.editor_font_bold = pygame.font.SysFont(self.ui_config['editor_font'][0], self.ui_config['editor_font'][1], bold=True) + + self.current_tab = 0 + self.open_tabs = [] + self.tabs = { + 'welcome': tab_welcome, + 'editor': tab_editor, + } + self.project = '' + if len(command_line_arguments) > 0: + self.project = command_line_arguments.pop() + if os.path.isfile(self.project): + self.open_tab('editor', self.project) + + pygame.display.set_caption("Bliss - " + ("no project" if self.project == '' else self.project)) + logo = pygame.image.load("icon.png") + pygame.display.set_icon(logo) + + self.screen = pygame.display.set_mode((640,360), pygame.RESIZABLE) + self.tab_surface = pygame.Surface((640,360-self.interface_font.get_height()*3), pygame.RESIZABLE) + + self.toasts = [['', None, 0]] # message (str), type ('info', 'error'), display start time + self.tab_palette = None + + def post_toast(self, string, toast_type): + if type(string) != str: + raise Exception('expected str as toast string') + if toast_type not in ('info', 'error'): + raise Exception('expected valid toast type (info or error)') + self.toasts.append([string, toast_type, time.time()]) + + def key_down(self, event): + print(event) + if event.unicode == '\x11': + self.running = False + elif event.unicode == '\x17': + self.tab_palette = None + self.close_tab(self.current_tab) + elif event.unicode == '\x14': + if type(self.tab_palette) == str: + self.tab_palette = None + else: + self.tab_palette = '' + elif event.key == pygame.K_TAB and pygame.K_LCTRL in self.keys: + print(self.current_tab) + self.current_tab += 1 + if self.current_tab >= len(self.open_tabs): + self.current_tab = 0 + elif self.tab_palette == None: + self.open_tabs[self.current_tab].key_down(self, event) + if self.tab_palette != None: + if event.key == pygame.K_BACKSPACE: + self.tab_palette = self.tab_palette[:-1] + elif event.key == pygame.K_RETURN: + command = shlex.split(self.tab_palette) + args = command[1:] + if len(args) == 1: + args = args[0] + elif len(args) == 0: + args = None + try: + self.tab_palette = None + command = command[0] + self.open_tab(command, args) + except KeyError as e: + self.post_toast(f"No tab named '{command}'.", 'error') + except Exception as e: + self.post_toast(f"Error creating '{command}': {e}", 'error') + + def text_input(self, event): + print(event) + if self.tab_palette != None: + self.tab_palette += event.text + else: + self.open_tabs[self.current_tab].text_input(self, event) + + def key_up(self, event): + print(event) + self.open_tabs[self.current_tab].key_up(self, event) + + def open_tab(self, name, data=None): + self.open_tabs.insert(self.current_tab + 1, self.tabs[name].Tab(self, data)) + self.current_tab += 1 + + def close_tab(self, id): + self.open_tabs.pop(id) + if self.current_tab >= len(self.open_tabs): + self.current_tab = len(self.open_tabs) - 1 + if len(self.open_tabs) == 0: + self.open_tab('welcome') + self.post_toast('Cannot close only tab - returning to Welcome screen.', 'error') + self.current_tab = 0 + + def mainloop(self): + self.keys = [] + if len(self.open_tabs) < 1: + self.open_tab('welcome', None) + self.current_tab = 0 + self.running = True + last_update = time.time() + while self.running: + self.screen.fill(self.ui_config['application_background']) + dt = time.time() - last_update + last_update = time.time() + + for event in pygame.event.get(): + #print(event) + if event.type == pygame.QUIT: + self.running = False + if event.type == pygame.VIDEORESIZE: + self.screen = pygame.display.set_mode((event.w, event.h), pygame.RESIZABLE) + if event.type == pygame.KEYDOWN: + self.key_down(event) + self.keys.append(event.key) + elif event.type == pygame.KEYUP: + self.key_up(event) + self.keys.remove(event.key) + elif event.type == pygame.TEXTINPUT: + self.text_input(event) + + self.open_tabs[self.current_tab].update(self, pygame, dt) + + w, h = pygame.display.get_surface().get_size() + status_bar_pos = tools.Point((0, h-self.interface_font.get_height()*1.5)) + pygame.draw.rect(self.screen, self.ui_config['status_bar_background'], pygame.Rect(status_bar_pos[0], status_bar_pos[1], w, self.interface_font.get_height()*1.5)) + pygame.draw.rect(self.screen, self.ui_config['tab_bar_background'], pygame.Rect(0, 0, w, self.interface_font.get_height()*1.5)) + tools.write_to_screen(self.screen, self.interface_font_bold, self.open_tabs[self.current_tab].status_text, status_bar_pos + tools.Point((self.interface_font.get_height()/4, self.interface_font.get_height()/4)), (True, self.ui_config['status_bar_foreground'])) + tools.write_to_screen(self.screen, self.interface_font, '[' + self.open_tabs[self.current_tab].tab_name + ']', status_bar_pos + tools.Point((self.interface_font.get_height()/4 + w/4*3, self.interface_font.get_height()/4)), (True, self.ui_config['status_bar_foreground'])) + + if self.tab_palette != None: + pygame.draw.rect(self.screen, self.ui_config['status_bar_foreground'], pygame.Rect(status_bar_pos[0], status_bar_pos[1], w, self.interface_font.get_height()*1.5)) + tools.write_to_screen(self.screen, self.interface_font_bold, 'Create tab', status_bar_pos + tools.Point((self.interface_font.get_height()/4, self.interface_font.get_height()/4)), (True, self.ui_config['status_bar_background'])) + tw, th = self.interface_font_bold.size('Create tab ') + tools.write_to_screen(self.screen, self.interface_font, self.tab_palette + '|', status_bar_pos + tools.Point((self.interface_font.get_height()/4 + tw, self.interface_font.get_height()/4)), (True, self.ui_config['status_bar_background'])) + + i = 0 + while i < len(self.toasts): + if time.time() - self.toasts[i][2] >= 3: + self.toasts.pop(i) + else: + i += 1 + + open_tab_number = len(self.open_tabs) + if open_tab_number > 0: + for i in range(open_tab_number): + tab_width = w/open_tab_number + tab_pos = tab_width*i + colour = self.ui_config['tab_bar_foreground'] + if self.current_tab == i: + colour = self.ui_config['tab_focus_foreground'] + pygame.draw.rect(self.screen, self.ui_config['tab_focus_background'], pygame.Rect(tab_pos, 0, tab_width, self.interface_font.get_height()*1.5)) + self.tab_y = self.interface_font.get_height()*1.5 + tools.write_to_screen(self.screen, self.interface_font, self.open_tabs[i].title, tools.Point((self.interface_font.get_height()/4 + tab_pos, self.interface_font.get_height()/4)), (True, colour)) + + self.tab_surface = pygame.transform.scale(self.tab_surface, (w, h-self.interface_font.get_height()*3)) + + self.open_tabs[self.current_tab].draw(self, pygame) + self.screen.blit(self.tab_surface, (0, self.tab_y)) + self.tab_surface.fill(self.ui_config['application_background']) + + for i in range(len(self.toasts)): + inv_i = len(self.toasts) - i - 1 + tw, th = self.interface_font.size(self.toasts[i][0]) + if time.time() - self.toasts[i][2] < 2.5 : + offset = 0 + else: + offset = ((time.time() - self.toasts[i][2] - 2.5)*2)**2 + pos = tools.Point((0-offset*(tw+15), h-self.interface_font.get_height()*(2.5+inv_i))) + pygame.draw.rect(self.screen, self.ui_config['toasts']['base_background'], pygame.Rect(pos[0], pos[1], tw + 15, self.interface_font.get_height())) + pygame.draw.rect(self.screen, self.ui_config['toasts'][self.toasts[i][1]][0], pygame.Rect(pos[0], pos[1], 5, self.interface_font.get_height())) + tools.write_to_screen(self.screen, self.interface_font, self.toasts[i][0], pos + tools.Point((10, 0)), (True, self.ui_config['toasts'][self.toasts[i][1]][1])) + + pygame.display.flip() + +if __name__ == '__main__': + app = App() + app.mainloop() \ No newline at end of file diff --git a/tabs/editor.py b/tabs/editor.py new file mode 100644 index 0000000..79b0cfb --- /dev/null +++ b/tabs/editor.py @@ -0,0 +1,118 @@ +import tools +import os +from random import random +from time import time +from math import sin, pi +from tifer.tifer import FileEditor +import pygame as pg + +class Tab: + def __init__(self, app, data): + self.title = 'Editor' + self.tab_name = 'editor' + self.status_text = f'{data}: L1:C1' + + self.file = data + + if self.file is not None: + if not os.path.isfile(self.file): + with open(self.file, 'w', encoding='utf-8') as f: + f.write('') + with open(self.file, 'r', encoding='utf-8') as f: + self.original_file = f.read() + self.editor = FileEditor(self.original_file) + self.visual_cursor = self.editor.cursor.copy() + self.scroll = [0,0] + self.visual_scroll = self.scroll.copy() + + def key_down(self, app, event): + if event.key == pg.K_RIGHT: + self.editor.move_xy(1,0) + elif event.key == pg.K_LEFT: + self.editor.move_xy(-1,0) + elif event.key == pg.K_UP: + self.editor.move_xy(0,-1) + elif event.key == pg.K_DOWN: + self.editor.move_xy(0,1) + elif event.key == pg.K_BACKSPACE: + self.editor.backspace(1) + elif event.key == pg.K_RETURN: + self.editor.write('\n') + elif event.key == pg.K_TAB: + self.editor.write(' ') + elif event.key == pg.K_s and pg.K_LCTRL in app.keys: + if '\n'.join([''.join(x) for x in self.editor.text]) != self.original_file: + with open(self.file, 'w') as f: + print('\n'.join([''.join(x) for x in self.editor.text])) + f.write('\n'.join([''.join(x) for x in self.editor.text])) + with open(self.file, 'r') as f: + self.original_file = f.read() + app.post_toast('Saved file.', 'info') + else: + app.post_toast('Cannot save unmodified file.', 'error') + def key_up(self, app, event): pass + + def text_input(self, app, event): + self.editor.write(event.text) + + def update(self, app, pygame, dt): + #print(self.visual_cursor, self.editor.cursor) + if self.visual_cursor is not None and self.editor is not None: + if self.visual_cursor[0] != self.editor.cursor[0]: + self.visual_cursor[0] += (self.editor.cursor[0] - self.visual_cursor[0])*(dt*20) + if abs(self.editor.cursor[0] - self.visual_cursor[0]) < 0.05: + self.visual_cursor[0] = self.editor.cursor[0] + + cl, cc = self.editor.cursor + tw, th = app.editor_font.size(str(''.join(self.editor.text[cl][:cc]))) + vtw = self.visual_cursor[1] + + if vtw != tw: + self.visual_cursor[1] += (tw - self.visual_cursor[1])*(dt*20) + if abs(tw - self.visual_cursor[1]) < 0.05: + self.visual_cursor[1] = tw + + + w, h = app.tab_surface.get_size() + while app.editor_font.get_height()*(self.editor.cursor[0]-self.scroll[0]) < 0: + self.scroll[0] -= 1 + while app.editor_font.get_height()*(self.editor.cursor[0]-self.scroll[0]) >= h: + self.scroll[0] += 1 + + if self.visual_scroll != self.scroll: + self.visual_scroll[0] = (self.visual_scroll[0]+self.scroll[0])/2 + self.visual_scroll[1] = (self.visual_scroll[1]+self.scroll[1])/2 + if abs(self.scroll[0] - self.visual_scroll[0]) < 0.05: + self.visual_scroll[0] = self.scroll[0] + if abs(self.scroll[1] - self.visual_scroll[1]) < 0.05: + self.visual_scroll[1] = self.scroll[1] + + mod = '' + if '\n'.join([''.join(x) for x in self.editor.text]) != self.original_file: + mod = '*' + self.status_text = f'{self.file} - L{self.editor.cursor[0] + 1}:C{self.editor.cursor[1] + 1} {mod}' + + def draw(self, app, pygame: pg): + if self.file != None: + cl, cc = self.editor.cursor + vcl, vcc = self.visual_cursor + self.title = str(self.file) + pad = app.editor_font.size('0 ') [0] + for l in range(len(self.editor.text)+1): + pw, ph = app.editor_font.size(str(l) + ' ') + pad = max(pad, pw) + spw, sph = app.editor_font_bold.size(' ') + w, h = app.tab_surface.get_size() + pygame.draw.rect(app.tab_surface, (app.ui_config['application_background_highlight']), pygame.Rect(0, app.editor_font.get_height()*(vcl-self.visual_scroll[0]), app.tab_surface.get_size()[0], app.editor_font.get_height())) + for l in range(len(self.editor.text)): + if app.editor_font.get_height()*(l-self.scroll[0]) < 0: + continue + if app.editor_font.get_height()*(l-self.scroll[0]) >= h: + break + if self.editor.cursor[0] == l: + tools.write_to_screen(app.tab_surface, app.editor_font_bold, str(l + 1), (spw/2, app.editor_font.get_height()*(l-self.visual_scroll[0])), (True, (200,)*3)) + else: + tools.write_to_screen(app.tab_surface, app.editor_font, str(l + 1), (0, app.editor_font.get_height()*(l-self.visual_scroll[0])), (True, (200,)*3)) + tools.write_to_screen(app.tab_surface, app.editor_font, ''.join(self.editor.text[l]), (pad, app.editor_font.get_height()*(l-self.visual_scroll[0])), (True, (255,)*3)) + tw, th = app.editor_font.size(str(''.join(self.editor.text[cl][:cc]))) + pygame.draw.line(app.tab_surface, (255,)*3, (pad+vcc-self.visual_scroll[1], app.editor_font.get_height()*(vcl-self.visual_scroll[0])), (pad+vcc-self.visual_scroll[1], app.editor_font.get_height()*(vcl+1-self.visual_scroll[0]))) \ No newline at end of file diff --git a/tabs/welcome.py b/tabs/welcome.py new file mode 100644 index 0000000..d22b8ea --- /dev/null +++ b/tabs/welcome.py @@ -0,0 +1,49 @@ +import tools +from random import random +from time import time +from math import sin, pi + +class Tab: + def __init__(self, app, data): + self.title = 'Welcome' + self.tab_name = 'welcome' + self.status_text = 'Bliss ' + app.VERSION + + self.visual_keybinds = [ + ['Ctrl+Q', 'Quit'], + ['Ctrl+TAB', 'Change tab'], + ['Ctrl+W', 'Exit tab'], + ['Ctrl+T', 'Create tab'], + ['Ctrl+B', 'Open project browser'], + ] + self.tips = [ + ['[Ctrl+T]welcome[ENTER]', 'Create a tab to this page'], + ['[Ctrl+T]editor [ENTER]', 'Create an editor tab'] + ] + + def key_down(self, app, event): pass + def key_up(self, app, event): pass + def text_input(self, app, event): pass + + def update(self, app, pygame, dt): pass + + def draw(self, app, pygame): + tools.write_to_screen(app.tab_surface, app.interface_font_bold, 'Welcome to Bliss!', (app.tab_y*(2+sin(time()*(pi/2))), 0), (True, (255,)*3)) + tools.write_to_screen(app.tab_surface, app.interface_font, 'Bliss is a new text editor built from the ground up using PyGame.', (app.tab_y, app.interface_font.get_height()*1), (True, (255,)*3)) + tools.write_to_screen(app.tab_surface, app.interface_font, 'Its main focus is style mixed with productivity.', (app.tab_y, app.interface_font.get_height()*2), (True, (255,)*3)) + + tools.write_to_screen(app.tab_surface, app.interface_font_bold, 'Key bindings', (app.tab_y, app.interface_font.get_height()*4), (True, (255,)*3)) + tools.write_to_screen(app.tab_surface, app.interface_font, 'Bliss limits the usage of the mouse for increased productivity.', (app.tab_y, app.interface_font.get_height()*5), (True, (255,)*3)) + for i in range(len(self.visual_keybinds)): + bind = self.visual_keybinds[i] + + tools.write_to_screen(app.tab_surface, app.interface_font_bold, f'[{bind[0]}]', (app.tab_y, app.interface_font.get_height()*(6+i)), (True, (255,)*3)) + tools.write_to_screen(app.tab_surface, app.interface_font, bind[1], (app.tab_y + 200, app.interface_font.get_height()*(6+i)), (True, (255,)*3)) + + tools.write_to_screen(app.tab_surface, app.interface_font_bold, 'Tips', (app.tab_y, app.interface_font.get_height()*(7 + len(self.visual_keybinds))), (True, (255,)*3)) + for i in range(len(self.tips)): + tip = self.tips[i] + + tools.write_to_screen(app.tab_surface, app.interface_font_bold, tip[0], (app.tab_y, app.interface_font.get_height()*(8+len(self.visual_keybinds)+i)), (True, (255,)*3)) + tools.write_to_screen(app.tab_surface, app.interface_font, tip[1], (app.tab_y + 200, app.interface_font.get_height()*(8+len(self.visual_keybinds)+i)), (True, (255,)*3)) + diff --git a/test.txt b/test.txt new file mode 100644 index 0000000..8bd7f86 --- /dev/null +++ b/test.txt @@ -0,0 +1,11 @@ +# i have successfully written this working python code with bliss +def main(): + print('poopoo') + +if __name__ == '__main__': + main() + +# small but very obvious and important issue: +# the cursor is always smooth +# and some users don't deal with animation very well +# so i want to make it a setting \ No newline at end of file diff --git a/tools.py b/tools.py index 42e50ac..7b7cdec 100644 --- a/tools.py +++ b/tools.py @@ -2,6 +2,11 @@ def write_to_screen(screen, font, text, pos, flags): img = font.render(text, *flags) screen.blit(img, pos) +def lerp(a, b, p): + return a + (b-a)*p +def unlerp(a, b, p): + return (p-a)/(b-a) + class Point(tuple): def __add__(self, other): if isinstance(other, tuple):