diff --git a/docs/gui.rst b/docs/gui.rst index 436db4dce7116..26e1302bc93d6 100644 --- a/docs/gui.rst +++ b/docs/gui.rst @@ -201,6 +201,37 @@ A *event filter* is a list combined of *key*, *type* and *(type, key)* tuple, e. gui.get_event((ti.GUI.PRESS, ti.GUI.ESCAPE), (ti.GUI.RELEASE, ti.GUI.SPACE)) +.. attribute:: gui.running + + :parameter gui: (GUI) + :return: (bool) ``True`` if ``ti.GUI.EXIT`` event occurred, vice versa + + ``ti.GUI.EXIT`` occurs when you click on the close (X) button of a window. + So ``gui.running`` will obtain ``False`` when the GUI is being closed. + + For example, loop until the close button is clicked: + + :: + + while gui.running: + render() + gui.set_image(pixels) + gui.show() + + + You can also close the window by manually setting ``gui.running`` to ``False``: + + :: + + while gui.running: + if gui.get_event(ti.GUI.ESCAPE): + gui.running = False + + render() + gui.set_image(pixels) + gui.show() + + .. function:: gui.get_event(a, ...) :parameter gui: (GUI) @@ -213,8 +244,8 @@ A *event filter* is a list combined of *key*, *type* and *(type, key)* tuple, e. :: - while gui.get_event(): - print('Event key', gui.event.key) + if gui.get_event(): + print('Got event, key =', gui.event.key) For example, loop until ESC is pressed: @@ -226,6 +257,7 @@ A *event filter* is a list combined of *key*, *type* and *(type, key)* tuple, e. gui.set_image(img) gui.show() + .. function:: gui.get_events(a, ...) :parameter gui: (GUI) @@ -244,6 +276,7 @@ A *event filter* is a list combined of *key*, *type* and *(type, key)* tuple, e. elif e.type in ['a', ti.GUI.LEFT]: ... + .. function:: gui.is_pressed(key, ...) :parameter gui: (GUI) @@ -264,6 +297,7 @@ A *event filter* is a list combined of *key*, *type* and *(type, key)* tuple, e. elif gui.is_pressed('d', ti.GUI.RIGHT): print('Go right!') + .. function:: gui.get_cursor_pos() :parameter gui: (GUI) @@ -276,6 +310,7 @@ A *event filter* is a list combined of *key*, *type* and *(type, key)* tuple, e. mouse_x, mouse_y = gui.get_cursor_pos() + Image I/O --------- diff --git a/examples/euler.py b/examples/euler.py index be5baf675cf68..9a564f133e862 100644 --- a/examples/euler.py +++ b/examples/euler.py @@ -449,7 +449,7 @@ def paint(): set_bc() n = 0 -while (1): +while gui.running: calc_dt() copy_to_old() for rk_step in range(2): diff --git a/examples/game_of_life.py b/examples/game_of_life.py index 62d542181c4b6..2802aafe2cedf 100644 --- a/examples/game_of_life.py +++ b/examples/game_of_life.py @@ -53,12 +53,12 @@ def render(): print('Press the spacebar to run.') render() -while True: +while gui.running: while gui.get_event(ti.GUI.PRESS): if gui.event.key == ti.GUI.SPACE: run() render() elif gui.event.key == ti.GUI.ESCAPE: - exit() + gui.running = False gui.set_image(img.to_numpy().astype(np.uint8)) gui.show() diff --git a/examples/keyboard.py b/examples/keyboard.py index 15bbcbbc6f68d..932d550298be0 100644 --- a/examples/keyboard.py +++ b/examples/keyboard.py @@ -5,10 +5,10 @@ gui = ti.GUI("Keyboard", res=(400, 400)) -while True: +while gui.running: while gui.get_event(ti.GUI.PRESS): if gui.event.key == ti.GUI.ESCAPE: - exit() + gui.running = False elif gui.event.key == ti.GUI.RMB: x, y = gui.event.pos diff --git a/examples/mpm128.py b/examples/mpm128.py index a9274d63409b1..833c816f93fa2 100644 --- a/examples/mpm128.py +++ b/examples/mpm128.py @@ -1,6 +1,8 @@ import taichi as ti import numpy as np + ti.init(arch=ti.gpu) # Try to run on GPU + quality = 1 # Use a larger value for higher-res simulations n_particles, n_grid = 9000 * quality ** 2, 128 * quality dx, inv_dx = 1 / n_grid, float(n_grid) @@ -105,7 +107,7 @@ def reset(): for frame in range(20000): while gui.get_event(ti.GUI.PRESS): if gui.event.key == 'r': reset() - elif gui.event.key == ti.GUI.ESCAPE: exit(0) + elif gui.event.key in [ti.GUI.ESCAPE, ti.GUI.EXIT]: exit(0) if gui.event is not None: gravity[None] = [0, 0] # if had any event if gui.is_pressed(ti.GUI.LEFT, 'a'): gravity[None][0] = -1 if gui.is_pressed(ti.GUI.RIGHT, 'd'): gravity[None][0] = 1 diff --git a/examples/mpm88.py b/examples/mpm88.py index eabbc4d7bce8e..e125ff942d45b 100644 --- a/examples/mpm88.py +++ b/examples/mpm88.py @@ -1,6 +1,5 @@ import taichi as ti import random - ti.init(arch=ti.gpu) dim = 2 @@ -73,21 +72,17 @@ def substep(): J[p] *= 1 + dt * new_C.trace() C[p] = new_C - -gui = ti.GUI("MPM88", (512, 512)) - for i in range(n_particles): x[i] = [random.random() * 0.4 + 0.2, random.random() * 0.4 + 0.2] v[i] = [0, -1] J[i] = 1 +gui = ti.GUI("MPM88", (512, 512)) for frame in range(20000): for s in range(50): grid_v.fill([0, 0]) grid_m.fill(0) substep() - gui.clear(0x112F41) - pos = x.to_numpy() - gui.circles(pos, radius=1.5, color=0x068587) + gui.circles(x.to_numpy(), radius=1.5, color=0x068587) gui.show() diff --git a/examples/mpm99.py b/examples/mpm99.py index da0c306daa933..6326e2d04029b 100644 --- a/examples/mpm99.py +++ b/examples/mpm99.py @@ -91,7 +91,7 @@ def initialize(): Jp[i] = 1 initialize() gui = ti.GUI("Taichi MLS-MPM-99", res=512, background_color=0x112F41) -while not gui.get_event(ti.GUI.ESCAPE): +while not gui.get_event(ti.GUI.ESCAPE, ti.GUI.EXIT): for s in range(int(2e-3 // dt)): substep() colors = np.array([0x068587, 0xED553B, 0xEEEEF0], dtype=np.uint32) diff --git a/examples/nbody_oscillator.py b/examples/nbody_oscillator.py index 6589111b5fc2e..935319fdee7db 100644 --- a/examples/nbody_oscillator.py +++ b/examples/nbody_oscillator.py @@ -42,7 +42,7 @@ def advance(dt: ti.f32): gui = ti.GUI("n-body", res=(400, 400)) initialize() -while not gui.get_event(ti.GUI.ESCAPE): +while not gui.get_event(ti.GUI.ESCAPE, ti.GUI.EXIT): _pos = pos.to_numpy() gui.circles(_pos, radius=1, color=0x66ccff) gui.show() diff --git a/examples/pbf2d.py b/examples/pbf2d.py index 22d80afe5bacb..4cca876e0ae5e 100644 --- a/examples/pbf2d.py +++ b/examples/pbf2d.py @@ -337,7 +337,7 @@ def main(): print(f'boundary={boundary} grid={grid_size} cell_size={cell_size}') gui = ti.GUI('PBF2D', screen_res) print_counter = 0 - while True: + while gui.running: move_board() run_pbf() print_counter += 1 diff --git a/examples/quadtree.py b/examples/quadtree.py index e1c481fda38b3..4e3db0cbafeff 100644 --- a/examples/quadtree.py +++ b/examples/quadtree.py @@ -56,7 +56,7 @@ def vec2_npf32(m): gui = ti.GUI('Quadtree', (RES, RES)) -while not gui.get_event(ti.GUI.PRESS): +while not gui.get_event(ti.GUI.ESCAPE, ti.GUI.EXIT): Broot.deactivate_all() pos = gui.get_cursor_pos() action(vec2_npf32(pos)) diff --git a/examples/taichi_logo.py b/examples/taichi_logo.py index 3c1f6b1b66623..a5d7c91247d84 100644 --- a/examples/taichi_logo.py +++ b/examples/taichi_logo.py @@ -55,6 +55,6 @@ def paint(): paint() gui = ti.GUI('Logo', (512, 512)) -while True: +while gui.get_event(ti.GUI.ESCAPE, ti.GUI.EXIT): gui.set_image(x.to_numpy()) gui.show() diff --git a/examples/waterwave.py b/examples/waterwave.py index ba5ddd0ed200e..ee198da30a28b 100644 --- a/examples/waterwave.py +++ b/examples/waterwave.py @@ -93,7 +93,7 @@ def paint(): gui = ti.GUI("Water Wave", shape) for frame in range(100000): for e in gui.get_events(ti.GUI.PRESS): - if e.key == ti.GUI.ESCAPE: + if e.key in [ti.GUI.ESCAPE, ti.GUI.EXIT]: exit() elif e.key == 'r': reset() diff --git a/python/taichi/misc/gui.py b/python/taichi/misc/gui.py index 0205ccd651a26..4b4f35a9d8dff 100644 --- a/python/taichi/misc/gui.py +++ b/python/taichi/misc/gui.py @@ -23,6 +23,7 @@ class Event: LMB = 'LMB' MMB = 'MMB' RMB = 'RMB' + EXIT = 'WMClose' RELEASE = False PRESS = True @@ -44,6 +45,12 @@ def __init__(self, name, res=512, background_color=0x0): self.core.set_profiler( ti.core.get_current_program().get_profiler()) + def __enter__(self): + return self + + def __exit__(self, type, val, tb): + self.core = None # dereference to call GUI::~GUI() + def clear(self, color=None): if color is None: color = self.background_color @@ -209,7 +216,8 @@ def get_event(self, *filter): for e in self.get_events(*filter): self.event = e return True - return False + else: + return False def get_events(self, *filter): filter = filter and GUI.EventFilter(*filter) or None @@ -254,10 +262,26 @@ def get_cursor_pos(self): return pos[0], pos[1] def has_key_pressed(self): + import warnings + warnings.warn( + 'gui.has_key_pressed() is deprecated, use gui.get_event() instead.', + DeprecationWarning, + stacklevel=3) if self.has_key_event(): self.get_key_event() # pop to update self.key_pressed return len(self.key_pressed) != 0 + @property + def running(self): + return not self.core.should_close + + @running.setter + def running(self, value): + if value: + self.core.should_close = 0 + elif not self.core.should_close: + self.core.should_close = 1 + def rgb_to_hex(c): to255 = lambda x: min(255, max(0, int(x * 255))) diff --git a/python/taichi/misc/image.py b/python/taichi/misc/image.py index 09cbb1fd98973..f3519dc886887 100644 --- a/python/taichi/misc/image.py +++ b/python/taichi/misc/image.py @@ -44,8 +44,12 @@ def imshow(img, window_name='Taichi'): img = img.to_numpy() assert len(img.shape) in [2, 3], "Image must be either RGB/RGBA or greyscale" - gui = ti.GUI(window_name, res=img.shape[:2]) - img = gui.cook_image(img) - while not gui.get_event(ti.GUI.ESCAPE): - gui.set_image(img) - gui.show() + + with ti.GUI(window_name, res=img.shape[:2]) as gui: + img = gui.cook_image(img) + while gui.running: + if gui.get_event(ti.GUI.ESCAPE): + gui.running = False + + gui.set_image(img) + gui.show() diff --git a/taichi/gui/gui.h b/taichi/gui/gui.h index b0390f1509bf3..e1f3d3457a2ad 100644 --- a/taichi/gui/gui.h +++ b/taichi/gui/gui.h @@ -428,6 +428,7 @@ class GUIBaseX11 { void *visual; unsigned long window; CXImage *img; + std::vector wmDeleteMessage; }; using GUIBase = GUIBaseX11; @@ -470,6 +471,7 @@ class GUI : public GUIBase { std::unique_ptr canvas; float64 last_frame_time; bool key_pressed; + int should_close{0}; std::vector log_entries; Vector2i cursor_pos; bool button_status[3]; @@ -705,6 +707,12 @@ class GUI : public GUIBase { void process_event(); + void send_window_close_message() { + key_events.push_back( + GUI::KeyEvent{GUI::KeyEvent::Type::press, "WMClose", cursor_pos}); + should_close++; + } + void mouse_event(MouseEvent e) { if (e.type == MouseEvent::Type::press) { button_status[0] = true; @@ -777,6 +785,18 @@ class GUI : public GUIBase { } last_frame_time = taichi::Time::get_time(); redraw(); + // Some old examples / users don't even provide a `break` statement for us + // to terminate loop. So we have to terminate the program with RuntimeError + // if ti.GUI.EXIT event is not processed. Pretty like SIGTERM, you can hook + // it, but you have to terminate after your handler is done. + if (should_close) { + if (++should_close > 5) { + // if the event is not processed in 5 frames, raise RuntimeError + throw std::string( + "Window close button clicked, exiting... (use `while gui.running` " + "to exit gracefully)"); + } + } process_event(); while (last_frame_interval.size() > 30) { last_frame_interval.erase(last_frame_interval.begin()); diff --git a/taichi/gui/win32.cpp b/taichi/gui/win32.cpp index 802311048ce32..fe4ac9ff4d835 100644 --- a/taichi/gui/win32.cpp +++ b/taichi/gui/win32.cpp @@ -69,10 +69,6 @@ LRESULT CALLBACK WindowProc(HWND hwnd, using namespace taichi; int x, y; switch (uMsg) { - case WM_DESTROY: - PostQuitMessage(0); - exit(0); - return 0; case WM_LBUTTONDOWN: gui->mouse_event( GUI::MouseEvent{GUI::MouseEvent::Type::press, gui->cursor_pos}); @@ -122,7 +118,8 @@ LRESULT CALLBACK WindowProc(HWND hwnd, gui->cursor_pos}); break; case WM_CLOSE: - exit(0); + // https://stackoverflow.com/questions/3155782/what-is-the-difference-between-wm-quit-wm-close-and-wm-destroy-in-a-windows-pr + gui->send_window_close_message(); break; } return DefWindowProc(hwnd, uMsg, wParam, lParam); diff --git a/taichi/gui/x11.cpp b/taichi/gui/x11.cpp index b8f2e0a02dc69..e3c78effb864c 100644 --- a/taichi/gui/x11.cpp +++ b/taichi/gui/x11.cpp @@ -3,6 +3,7 @@ #if defined(TI_GUI_X11) #include #include +#include // Undo terrible unprefixed macros in X.h #ifdef None @@ -73,6 +74,12 @@ void GUI::process_event() { switch (ev.type) { case Expose: break; + case ClientMessage: + // https://stackoverflow.com/questions/10792361/how-do-i-gracefully-exit-an-x11-event-loop + if (ev.xclient.data.l[0] == *(Atom *)wmDeleteMessage.data()) { + send_window_close_message(); + } + break; case MotionNotify: set_mouse_pos(ev.xbutton.x, height - ev.xbutton.y - 1); mouse_event(MouseEvent{MouseEvent::Type::move, cursor_pos}); @@ -114,6 +121,11 @@ void GUI::create_window() { ButtonPressMask | ExposureMask | KeyPressMask | KeyReleaseMask | ButtonPress | ButtonReleaseMask | EnterWindowMask | LeaveWindowMask | PointerMotionMask); + wmDeleteMessage = std::vector(sizeof(Atom)); + *(Atom *)wmDeleteMessage.data() = + XInternAtom((Display *)display, "WM_DELETE_WINDOW", False); + XSetWMProtocols((Display *)display, window, (Atom *)wmDeleteMessage.data(), + 1); XMapWindow((Display *)display, window); img = new CXImage((Display *)display, (Visual *)visual, width, height); } @@ -129,6 +141,7 @@ void GUI::set_title(std::string title) { } GUI::~GUI() { + XCloseDisplay((Display *)display); delete img; } diff --git a/taichi/python/export_visual.cpp b/taichi/python/export_visual.cpp index f28148b417129..3ad2d66a8534c 100644 --- a/taichi/python/export_visual.cpp +++ b/taichi/python/export_visual.cpp @@ -15,6 +15,7 @@ void export_visual(py::module &m) { using Circle = Canvas::Circle; py::class_(m, "GUI") .def(py::init()) + .def_readwrite("should_close", &GUI::should_close) .def("get_canvas", &GUI::get_canvas, py::return_value_policy::reference) .def("set_img", [&](GUI *gui, std::size_t ptr) {