├── .gitignore ├── CHANGELOG.md ├── MANIFEST.in ├── README.md ├── docs └── images │ └── toolgui.gif ├── examples ├── 1_hello_world.py └── 2_more_examples.py ├── package.sh ├── setup.py └── toolgui ├── Roboto-Regular.ttf ├── __init__.py ├── _app.py ├── _data.py └── _gui.py /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | venv/ 3 | imgui.ini 4 | toolgui.ini 5 | __pycache__/ 6 | *.egg-info/ 7 | build/ 8 | dist/ 9 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [0.0.10] - 2022-04-30 8 | - Fixed missing font 9 | 10 | ## [0.0.9] - 2022-04-30 11 | - Updated default style 12 | 13 | ## [0.0.8] - 2022-03-26 14 | - Fix crash for menu item names with no "/" character 15 | - Added more usage instructions to readme 16 | 17 | ## [0.0.7] - 2021-03-21 18 | - Wrapped boilerplate with the `@toolgui.window` decorator 19 | 20 | ## [0.0.6] - 2021-03-17 21 | - Added changelog 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include toolgui *.ttf -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # toolgui 2 | 3 | Modular event-driven GUI system for quickly building tools with Python and [pyimgui](https://pyimgui.readthedocs.io/). 4 | 5 | ![](https://github.com/rempelj/toolgui/raw/master/docs/images/toolgui.gif) 6 | 7 | ## Installation 8 | 9 | ``` 10 | pip install toolgui 11 | ``` 12 | 13 | ## Usage 14 | ### Window 15 | 16 | Create a window that can be opened from the menu bar. 17 | 18 | ```python 19 | import imgui 20 | import toolgui 21 | 22 | @toolgui.window("Example/Hello World") 23 | def hello_example(): 24 | imgui.text("Hello!") 25 | 26 | toolgui.set_app_name("Hello World Example") 27 | toolgui.start_toolgui_app() 28 | ``` 29 | 30 | ### Settings 31 | 32 | Persist state across sessions. Data is saved to the `toolgui.ini` file. 33 | 34 | ```python 35 | import imgui 36 | import toolgui 37 | 38 | @toolgui.settings("Number Picker") 39 | class Settings: 40 | my_number = 0 41 | 42 | @toolgui.window("Example/Number Picker") 43 | def number_picker(): 44 | Settings.my_number = imgui.input_int("My Number", Settings.my_number, 1)[1] 45 | 46 | toolgui.set_app_name("Number Picker Example") 47 | toolgui.start_toolgui_app() 48 | 49 | ``` 50 | 51 | ### Menu Item 52 | 53 | Call a static function from the menu bar. 54 | 55 | ```python 56 | @toolgui.menu_item("Example/Reset") 57 | def reset(): 58 | Settings.my_number = 0 59 | ``` 60 | 61 | ### Events 62 | Event functions are executed by toolgui with these decorators. 63 | 64 | #### Application Start 65 | Executed when the application starts. 66 | ```python 67 | @toolgui.on_app_start() 68 | def on_app_start(): 69 | print("Application started") 70 | ``` 71 | 72 | #### Application Quit 73 | Executed when the application quits. 74 | ```python 75 | @toolgui.on_app_quit() 76 | def on_app_quit(): 77 | print("Application quit") 78 | ``` 79 | 80 | #### Update 81 | Executed every frame. 82 | ```python 83 | @toolgui.on_update() 84 | def on_update(): 85 | # do_something() 86 | ``` 87 | -------------------------------------------------------------------------------- /docs/images/toolgui.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rjtngit/toolgui/d6fe626548832fb44cea5ece8d0650bd07e196d8/docs/images/toolgui.gif -------------------------------------------------------------------------------- /examples/1_hello_world.py: -------------------------------------------------------------------------------- 1 | import imgui 2 | import toolgui 3 | 4 | @toolgui.window("Example/Hello World") 5 | def hello_example(): 6 | imgui.text("Hello!") 7 | 8 | toolgui.set_app_name("Hello World Example") 9 | toolgui.start_toolgui_app() 10 | -------------------------------------------------------------------------------- /examples/2_more_examples.py: -------------------------------------------------------------------------------- 1 | import imgui 2 | import toolgui 3 | 4 | 5 | @toolgui.settings("Number Picker") 6 | class Settings: 7 | my_number = 0 8 | 9 | 10 | class State: 11 | frame_count = 0 12 | 13 | 14 | @toolgui.window("Example/Windows/Number Picker") 15 | def number_picker(): 16 | imgui.text(f"Frame count: {State.frame_count}") 17 | Settings.my_number = imgui.input_int("My Number", Settings.my_number, 1)[1] 18 | 19 | 20 | @toolgui.menu_item("Example/Reset") 21 | def reset(): 22 | State.frame_count = 0 23 | Settings.my_number = 0 24 | 25 | 26 | @toolgui.on_app_start() 27 | def on_app_start(): 28 | print("Application started") 29 | 30 | 31 | @toolgui.on_app_quit() 32 | def on_app_quit(): 33 | print("Application quit") 34 | 35 | 36 | @toolgui.on_update() 37 | def on_update(): 38 | State.frame_count += 1 39 | 40 | 41 | toolgui.set_app_name("More Examples") 42 | toolgui.start_toolgui_app() 43 | -------------------------------------------------------------------------------- /package.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | script_dir=$(dirname "$0") 4 | cd "$script_dir" || exit 5 | 6 | rm -rf build 7 | rm -rf dist 8 | 9 | python setup.py sdist bdist_wheel 10 | 11 | twine upload dist/* 12 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | with open("README.md", "r") as fh: 4 | long_description = fh.read() 5 | 6 | setup( 7 | name="toolgui", 8 | version="0.0.10", 9 | description="Modular event-driven GUI system for quickly building tools with Python and pyimgui.", 10 | packages=find_packages(), 11 | package_data={'': ['Roboto-Regular.ttf']}, 12 | include_package_data=True, 13 | long_description=long_description, 14 | long_description_content_type="text/markdown", 15 | url="https://github.com/rempelj/toolgui", 16 | author="Justin Rempel", 17 | classifiers=[ 18 | ], 19 | install_requires=[ 20 | "imgui[glfw]", 21 | ], 22 | extras_require={ 23 | "dev": [ 24 | "twine", 25 | "wheel", 26 | ], 27 | }, 28 | ) 29 | -------------------------------------------------------------------------------- /toolgui/Roboto-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rjtngit/toolgui/d6fe626548832fb44cea5ece8d0650bd07e196d8/toolgui/Roboto-Regular.ttf -------------------------------------------------------------------------------- /toolgui/__init__.py: -------------------------------------------------------------------------------- 1 | from ._app import start_toolgui_app 2 | from ._app import set_app_name 3 | from ._app import on_update 4 | from ._app import on_app_start 5 | from ._app import on_app_quit 6 | from ._data import StaticData 7 | from ._data import settings 8 | from ._gui import window 9 | from ._gui import menu_item 10 | 11 | -------------------------------------------------------------------------------- /toolgui/_app.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import glfw 4 | import OpenGL.GL as gl 5 | 6 | import imgui 7 | from imgui.integrations.glfw import GlfwRenderer 8 | 9 | import toolgui 10 | 11 | 12 | class State: 13 | app_name = "toolgui" 14 | style = {} 15 | update_callbacks = [] 16 | quit_callbacks = [] 17 | start_callbacks = [] 18 | 19 | 20 | def on_update(): 21 | """ 22 | @on_update() 23 | 24 | Decorator to add a static function to the event loop to be called every frame. 25 | """ 26 | def dec(callback): 27 | State.update_callbacks.append(callback) 28 | return dec 29 | 30 | def on_app_quit(): 31 | """ 32 | @on_app_quit() 33 | 34 | Decorator to call a static function when the application quits. 35 | """ 36 | def dec(callback): 37 | State.quit_callbacks.append(callback) 38 | return dec 39 | 40 | def on_app_start(): 41 | """ 42 | @on_app_start() 43 | 44 | Decorator to call a static function when the application starts. 45 | """ 46 | def dec(callback): 47 | State.start_callbacks.append(callback) 48 | return dec 49 | 50 | 51 | def init_style(window, impl): 52 | # font 53 | win_w, win_h = glfw.get_window_size(window) 54 | fb_w, fb_h = glfw.get_framebuffer_size(window) 55 | font_scaling_factor = max(float(fb_w) / win_w, float(fb_h) / win_h) 56 | font_size_in_pixels = 14 57 | io = imgui.get_io() 58 | font_path = os.path.join(os.path.dirname(toolgui.__file__), "Roboto-Regular.ttf") 59 | State.style["font"] = io.fonts.add_font_from_file_ttf( 60 | font_path, font_size_in_pixels * font_scaling_factor, 61 | io.fonts.get_glyph_ranges_default() 62 | ) 63 | io.font_global_scale /= font_scaling_factor 64 | impl.refresh_font_texture() 65 | 66 | # colors 67 | style = imgui.get_style() 68 | imgui.style_colors_light(style) 69 | style.colors[imgui.COLOR_BORDER] = (1, 1, 1, 1) 70 | 71 | 72 | def push_style(): 73 | imgui.push_font(State.style["font"]) 74 | 75 | 76 | def pop_style(): 77 | imgui.pop_font() 78 | 79 | 80 | def start_toolgui_app(): 81 | """ 82 | Start the application. 83 | """ 84 | imgui.create_context() 85 | window = _impl_glfw_init() 86 | impl = GlfwRenderer(window) 87 | 88 | init_style(window, impl) 89 | 90 | for on_start in State.start_callbacks: 91 | on_start() 92 | 93 | while not glfw.window_should_close(window): 94 | glfw.poll_events() 95 | impl.process_inputs() 96 | 97 | imgui.new_frame() 98 | 99 | push_style() 100 | for on_update in State.update_callbacks: 101 | on_update() 102 | pop_style() 103 | 104 | gl.glClearColor(0, 0, 0, 0) 105 | gl.glClear(gl.GL_COLOR_BUFFER_BIT) 106 | imgui.render() 107 | impl.render(imgui.get_draw_data()) 108 | glfw.swap_buffers(window) 109 | 110 | for on_quit in State.quit_callbacks: 111 | on_quit() 112 | 113 | impl.shutdown() 114 | glfw.terminate() 115 | 116 | 117 | def _impl_glfw_init(): 118 | width, height = 800, 600 119 | window_name = State.app_name 120 | 121 | if not glfw.init(): 122 | print("Could not initialize OpenGL context") 123 | exit(1) 124 | 125 | # OS X supports only forward-compatible core profiles from 3.2 126 | glfw.window_hint(glfw.CONTEXT_VERSION_MAJOR, 3) 127 | glfw.window_hint(glfw.CONTEXT_VERSION_MINOR, 3) 128 | glfw.window_hint(glfw.OPENGL_PROFILE, glfw.OPENGL_CORE_PROFILE) 129 | 130 | glfw.window_hint(glfw.OPENGL_FORWARD_COMPAT, gl.GL_TRUE) 131 | 132 | # Create a windowed mode window and its OpenGL context 133 | window = glfw.create_window( 134 | int(width), int(height), window_name, None, None 135 | ) 136 | glfw.make_context_current(window) 137 | 138 | if not window: 139 | glfw.terminate() 140 | print("Could not initialize Window") 141 | exit(1) 142 | 143 | return window 144 | 145 | 146 | def set_app_name(name): 147 | """ 148 | Set the name of the application. 149 | """ 150 | State.app_name = name 151 | 152 | -------------------------------------------------------------------------------- /toolgui/_data.py: -------------------------------------------------------------------------------- 1 | import toolgui 2 | import configparser 3 | import ast 4 | 5 | _UNSET = object() 6 | _LITERALS = (int, float, bool, str) 7 | 8 | 9 | 10 | @toolgui.on_app_start() 11 | def init_default_data(): 12 | for cls in get_all_subclasses(StaticData): 13 | cls._init() 14 | 15 | def get_all_subclasses(cls): 16 | all_subclasses = [] 17 | 18 | for subclass in cls.__subclasses__(): 19 | all_subclasses.append(subclass) 20 | all_subclasses.extend(get_all_subclasses(subclass)) 21 | 22 | return all_subclasses 23 | 24 | class StaticData: 25 | @classmethod 26 | def _init(cls): 27 | cls._defaults = {} 28 | for var in vars(cls): 29 | if not var.startswith("_"): 30 | curval = getattr(cls, var) 31 | if type(curval) in _LITERALS: 32 | cls._defaults[var] = curval 33 | 34 | @classmethod 35 | def reset(cls): 36 | for var in vars(cls): 37 | if not var.startswith("_"): 38 | curval = getattr(cls, var) 39 | if type(curval) in _LITERALS: 40 | default_val = cls._defaults[var] 41 | setattr(cls, var, default_val) 42 | 43 | @classmethod 44 | def count_values(cls): 45 | result = 0 46 | for var in dir(cls): 47 | if not var.startswith("_"): 48 | val = getattr(cls, var) 49 | if val and type(val) in _LITERALS: 50 | result += 1 51 | return result 52 | 53 | @classmethod 54 | def has_any_values(cls): 55 | return cls.count_values() > 0 56 | 57 | 58 | def settings(name): 59 | """ 60 | @settings(name) 61 | 62 | Decorator to serialize a class's static variables in the toolgui.ini file. 63 | """ 64 | def dec(settings_class): 65 | @toolgui.on_app_quit() 66 | def save_settings(): 67 | config = configparser.ConfigParser() 68 | config.read("toolgui.ini") 69 | if not config.has_section(name): 70 | config.add_section(name) 71 | for var in dir(settings_class): 72 | if not var.startswith("_"): 73 | val = getattr(settings_class, var) 74 | config.set(name, var, repr(val)) 75 | with open("toolgui.ini", 'w+') as configfile: 76 | config.write(configfile) 77 | 78 | @toolgui.on_app_start() 79 | def load_settings(): 80 | config = configparser.ConfigParser() 81 | config.read("toolgui.ini") 82 | if not config.has_section(name): 83 | config.add_section(name) 84 | for var in dir(settings_class): 85 | if not var.startswith("_"): 86 | val = config.get(name, var, fallback=_UNSET) 87 | if val is not _UNSET: 88 | try: 89 | setattr(settings_class, var, ast.literal_eval(val)) 90 | except ValueError as exc: 91 | print(f"Malformed settings data: " 92 | f"{settings_class.__name__}." 93 | f"{var} = {str(val)}") 94 | return settings_class 95 | return dec -------------------------------------------------------------------------------- /toolgui/_gui.py: -------------------------------------------------------------------------------- 1 | import imgui 2 | import toolgui 3 | 4 | class State: 5 | menu_top_nodes = [] 6 | 7 | 8 | class MenuNode: 9 | def __init__(self, path, name, callback=None): 10 | self.path = path 11 | self.name = name 12 | self.callback = callback 13 | self.children = [] 14 | 15 | 16 | def menu_item(path): 17 | """ 18 | @menu_item(Path) 19 | 20 | Decorator to call a static function from the main menu. 21 | Use forward slashes (/) to separate menu levels. 22 | """ 23 | def dec(callback): 24 | add_menu_data(path, callback) 25 | return dec 26 | 27 | 28 | def add_menu_data(path, callback): 29 | first_node_name = path.split("/", 1)[0] 30 | try: 31 | path_remainder = path.split("/", 1)[1] 32 | except Exception: 33 | path_remainder = first_node_name 34 | try: 35 | last_node_name = path.rsplit("/", 1)[1] 36 | except Exception: 37 | last_node_name = first_node_name 38 | 39 | leaf_node = MenuNode(path, last_node_name, callback) 40 | for node in State.menu_top_nodes: 41 | if first_node_name == node.name and not node.callback: 42 | build_tree(node, path_remainder, leaf_node) 43 | return 44 | first_node = MenuNode(first_node_name, first_node_name) 45 | build_tree(first_node, path_remainder, leaf_node) 46 | State.menu_top_nodes.append(first_node) 47 | 48 | 49 | def build_tree(from_node, path, end_node): 50 | if path == end_node.name: 51 | from_node.children.append(end_node) 52 | return 53 | next_node_name = path.split("/", 1)[0] 54 | path_remainder = path.split("/", 1)[1] 55 | for child in from_node.children: 56 | if next_node_name == child.name and not child.callback: 57 | build_tree(child, path_remainder, end_node) 58 | return 59 | next_node = MenuNode(from_node.path + "/" + next_node_name, next_node_name) 60 | build_tree(next_node, path_remainder, end_node) 61 | from_node.children.append(next_node) 62 | 63 | 64 | def update_menu_from_node(node): 65 | if node.callback: 66 | clicked, selected = imgui.menu_item(node.name) 67 | if clicked: 68 | node.callback() 69 | else: 70 | if imgui.begin_menu(node.name): 71 | for i, child in enumerate(node.children): 72 | update_menu_from_node(child) 73 | imgui.end_menu() 74 | 75 | 76 | @toolgui.on_update() 77 | def update_main_menu(): 78 | if imgui.begin_main_menu_bar(): 79 | for node in State.menu_top_nodes: 80 | update_menu_from_node(node) 81 | imgui.end_main_menu_bar() 82 | 83 | 84 | def window(menu_path): 85 | """ 86 | @window_update(menu_path) 87 | 88 | Decorator for the update loop of a window that can be opened from the main menu bar. 89 | """ 90 | def dec(update_func): 91 | @toolgui.settings(menu_path) 92 | class Settings: 93 | window_open = False 94 | 95 | @toolgui.menu_item(menu_path) 96 | def show_window(): 97 | Settings.window_open = True 98 | 99 | @toolgui.on_update() 100 | def update_window(): 101 | if Settings.window_open: 102 | try: 103 | window_name = menu_path.rsplit("/", 1)[1] 104 | except Exception: 105 | window_name = menu_path.rsplit("/", 1)[0] 106 | expanded, Settings.window_open = imgui.begin(window_name, True) 107 | update_func() 108 | imgui.end() 109 | return dec --------------------------------------------------------------------------------