Ejemplo n.º 1
0
class Button(Component, ABC):
    hover: RV[bool] = rv.new_view()

    active: RV[bool] = rv.new_view()

    def __init__(self, context: Context, visible: bool = True) -> None:
        super().__init__(context, visible)

        # noinspection PyTypeChecker
        self.hover = rx.merge(
            self.on_mouse_over.pipe(ops.map(lambda _: True)),
            self.on_mouse_out.pipe(ops.map(lambda _: False))).pipe(ops.start_with(False))

        mouse = MouseInput.input(self)

        # noinspection PyTypeChecker
        self.active = self.on_mouse_down.pipe(
            ops.filter(lambda e: e.button == MouseButton.LEFT),
            ops.map(lambda _: rx.concat(rx.of(True), mouse.on_button_release(MouseButton.LEFT).pipe(
                ops.take(1),
                ops.map(lambda _: False)))),
            ops.exclusive(),
            ops.start_with(False))

    def dispatch_event(self, event: Event) -> None:
        if isinstance(event, PropagatingEvent):
            event.stop_propagation()

        super().dispatch_event(event)

    @property
    def style_fallback_prefixes(self) -> Iterable[str]:
        return chain(["Button"], super().style_fallback_prefixes)
Ejemplo n.º 2
0
class FixtureContext(Context):
    window_size: RV[Dimension] = rv.new_view()

    def __init__(self,
                 size: Dimension,
                 toolkit: FixtureToolkit,
                 look_and_feel: Optional[LookAndFeel] = None,
                 font_options: Optional[FontOptions] = None,
                 window_manager: Optional[WindowManager] = None,
                 error_handler: Optional[ErrorHandler] = None) -> None:
        if size is None:
            raise ValueError("Argument 'size' is required.")

        # noinspection PyTypeChecker
        self.window_size = rx.of(size)

        super().__init__(toolkit, look_and_feel, font_options, window_manager,
                         error_handler)
Ejemplo n.º 3
0
class BlenderKeyInput(KeyInput, ReactiveObject, EventLoopAware):
    pressed: RV[Set[int]] = rv.new_view()

    def __init__(self, context: BlenderContext) -> None:
        super().__init__(context)

        self._activeInputs = Subject()

        # noinspection PyTypeChecker
        self.pressed = self._activeInputs.pipe(
            ops.start_with({}), ops.map(lambda s: set(s.keys())))

    def process(self) -> None:
        self._activeInputs.on_next(keyboard.activeInputs)

    def dispose(self) -> None:
        super().dispose()

        self.execute_safely(self._activeInputs.dispose)
Ejemplo n.º 4
0
class WindowManager(Drawable, ErrorHandlerSupport, ReactiveObject):
    windows: RV[Sequence[Window]] = rv.new_view()

    def __init__(self, error_handler: ErrorHandler) -> None:
        if error_handler is None:
            raise ValueError("Argument 'error_handler' is required.")

        super().__init__()

        self._error_handler = error_handler

        self._added_window = Subject()
        self._removed_window = Subject()

        changed_window = rx.merge(
            self._added_window.pipe(ops.map(lambda v: (v, True))),
            self._removed_window.pipe(ops.map(lambda v: (v, False))))

        def on_window_change(windows: Tuple[Window, ...], event: Tuple[Window, bool]):
            (window, added) = event

            if added and window not in windows:
                return windows + (window,)
            elif not added and window in windows:
                return tuple(c for c in windows if c is not window)

        # noinspection PyTypeChecker
        self.windows = changed_window.pipe(
            ops.scan(on_window_change, ()), ops.start_with(()), ops.distinct_until_changed())

    def add(self, window: Window) -> None:
        if window is None:
            raise ValueError("Argument 'window' is required.")

        self._added_window.on_next(window)

    def remove(self, window: Window) -> None:
        if window is None:
            raise ValueError("Argument 'window' is required.")

        self._removed_window.on_next(window)

    def window_at(self, location: Point) -> Maybe[Window]:
        if location is None:
            raise ValueError("Argument 'location' is required.")

        try:
            # noinspection PyTypeChecker
            children: Iterator[Window] = reversed(self.windows)

            return Some(next(c for c in children if c.bounds.contains(location)))
        except StopIteration:
            return Nothing

    def draw(self, g: Graphics) -> None:
        # noinspection PyTypeChecker
        for window in self.windows:
            window.validate()
            window.draw(g)

    @property
    def error_handler(self) -> ErrorHandler:
        return self._error_handler

    def dispose(self) -> None:
        # noinspection PyTypeChecker
        for window in self.windows:
            self.execute_safely(window.dispose)

        super().dispose()
Ejemplo n.º 5
0
class Context(EventLoopAware, ReactiveObject, InputLookup, ErrorHandlerSupport, ABC):
    window_size: RV[Dimension]

    surface: RV[Surface] = rv.new_view()

    graphics: RV[Graphics] = surface.map(lambda c, s: c.create_graphics(s))

    def __init__(self,
                 toolkit: Toolkit,
                 look_and_feel: Optional[LookAndFeel] = None,
                 font_options: Optional[FontOptions] = None,
                 window_manager: Optional[WindowManager] = None,
                 error_handler: Optional[ErrorHandler] = None) -> None:
        if toolkit is None:
            raise ValueError("Argument 'toolkit' is required.")

        super().__init__()

        from alleycat.ui import WindowManager
        from alleycat.ui.glass import GlassLookAndFeel

        self._toolkit = toolkit

        self._look_and_feel = Maybe.from_optional(look_and_feel).or_else_call(lambda: GlassLookAndFeel(toolkit))
        self._font_options = Maybe.from_optional(font_options).or_else_call(
            lambda: FontOptions(antialias=ANTIALIAS_SUBPIXEL, hint_style=HINT_STYLE_FULL))

        self._error_handler = Maybe.from_optional(error_handler).value_or(toolkit.error_handler)

        self._window_manager = Maybe.from_optional(window_manager) \
            .or_else_call(lambda: WindowManager(self.error_handler))

        inputs = toolkit.create_inputs(self)

        assert inputs is not None

        self._inputs = {i.id: i for i in inputs}
        self._pollers = [i for i in inputs if isinstance(i, EventLoopAware)]

        # noinspection PyTypeChecker
        self.surface = self.observe("window_size").pipe(ops.map(self.toolkit.create_surface))

        old_surface = self.observe("surface").pipe(
            ops.pairwise(),
            ops.map(lambda s: s[0]),
            ops.take_until(self.on_dispose))

        old_surface.subscribe(Surface.finish, on_error=self.error_handler)

    @property
    def toolkit(self) -> Toolkit:
        return self._toolkit

    @property
    def inputs(self) -> Mapping[str, Input]:
        return self._inputs

    @property
    def look_and_feel(self) -> LookAndFeel:
        return self._look_and_feel

    @property
    def font_options(self) -> FontOptions:
        return self._font_options

    @property
    def window_manager(self) -> WindowManager:
        return self._window_manager

    @property
    def error_handler(self) -> ErrorHandler:
        return self._error_handler

    # noinspection PyMethodMayBeStatic
    def create_graphics(self, surface: Surface):
        if surface is None:
            raise ValueError("Argument 'surface' is required.")

        g = Graphics(surface)

        g.set_antialias(ANTIALIAS_BEST)
        g.set_font_options(self.font_options)

        return g

    def process(self) -> None:
        self.execute_safely(self.process_inputs)
        self.execute_safely(self.process_draw)

    def process_inputs(self) -> None:
        for poller in self._pollers:
            self.execute_safely(poller.process)

    def process_draw(self) -> None:
        (width, height) = self.window_size.tuple

        g: Graphics = self.graphics

        op = g.get_operator()

        g.rectangle(0, 0, width, height)
        g.set_source_rgba(0, 0, 0, 0)
        g.set_operator(OPERATOR_CLEAR)
        g.fill()

        g.set_operator(op)

        self.window_manager.draw(g)

        self.surface.flush()

    def dispatcher_at(self, location: Point) -> Maybe[EventDispatcher]:
        if location is None:
            raise ValueError("Argument 'location' is required.")

        return self._window_manager.window_at(location).bind(lambda w: w.component_at(location))

    def dispose(self) -> None:
        super().dispose()

        self.execute_safely(self._window_manager.dispose)

        for i in self.inputs.values():
            self.execute_safely(i.dispose)
Ejemplo n.º 6
0
class Container(Component):
    children: RV[Sequence[Component]] = rv.new_view()

    def __init__(self,
                 context: Context,
                 layout: Optional[Layout] = None,
                 visible: bool = True):
        from .layout import AbsoluteLayout

        self._layout = Maybe.from_optional(layout).or_else_call(AbsoluteLayout)
        self._layout_pending = True
        self._layout_running = False

        # noinspection PyTypeChecker
        self.children = self.layout.observe("children").pipe(
            ops.map(
                lambda children: tuple(map(lambda c: c.component, children))))

        super().__init__(context, visible)

        self.observe("size") \
            .pipe(ops.filter(lambda _: self.visible), ops.distinct_until_changed()) \
            .subscribe(lambda _: self.request_layout(), on_error=self.error_handler)

    @property
    def layout(self) -> Layout:
        return self._layout

    def validate(self, force: bool = False) -> None:
        if self.visible and (not self.valid or force):
            self.request_layout()

            # noinspection PyTypeChecker
            for child in self.children:
                child.validate(force)

        super().validate(force)

    def invalidate(self) -> None:
        if not self._layout_running:
            super().invalidate()

    @property
    def layout_pending(self) -> bool:
        return self._layout_pending or not self.valid

    def request_layout(self) -> None:
        self._layout_pending = True

    def perform_layout(self) -> None:
        self._layout_running = True

        try:
            self.layout.perform(self.bounds.copy(x=0, y=0))
        except BaseException as e:
            self.context.error_handler(e)

        self._layout_pending = False
        self._layout_running = False

    def component_at(self, location: Point) -> Maybe[Component]:
        if location is None:
            raise ValueError("Argument 'location' is required.")

        if self.bounds.contains(location):
            try:
                # noinspection PyTypeChecker
                children: Iterator[Component] = reversed(self.children)

                child = next(c for c in children
                             if c.bounds.contains(location - self.location))

                if isinstance(child, Container):
                    return child.component_at(location - self.location)
                else:
                    return Some(child)
            except StopIteration:
                return Some(self)

        return Nothing

    def add(self, child: Component, *args, **kwargs) -> None:
        child.parent.map(lambda p: p.remove(child))

        self.layout.add(child, *args, **kwargs)

        child.parent = Some(self)

    def remove(self, child: Component) -> None:
        self.layout.remove(child)

        child.parent = Nothing

    def draw(self, g: Graphics) -> None:
        if self.layout_pending:
            self.perform_layout()

        super().draw(g)

    def draw_component(self, g: Graphics) -> None:
        super().draw_component(g)

        self.draw_children(g)

        cast(ContainerUI, self.ui).post_draw(g, self)

    def draw_children(self, g: Graphics) -> None:
        ui = cast(ContainerUI, self.ui)

        (cx, cy, cw, ch) = ui.content_bounds(self).tuple

        g.save()

        g.rectangle(cx, cy, cw, ch)
        g.clip()

        try:
            # noinspection PyTypeChecker
            for child in self.children:
                child.draw(g)
        except BaseException as e:
            self.error_handler(e)

        g.restore()

    def dispose(self) -> None:
        # noinspection PyTypeChecker
        for child in self.children:
            self.execute_safely(child.dispose)

        self.execute_safely(self.layout.dispose)

        super().dispose()
Ejemplo n.º 7
0
class BlenderContext(Context):
    window_size: RV[Dimension] = rv.new_view()

    batch: RV[GPUBatch] = window_size.map(lambda c, s: c.create_batch(s))

    buffer: RV[GPUBatch] = window_size.map(lambda c, s: c.create_batch(s))

    def __init__(self,
                 toolkit: BlenderToolkit,
                 look_and_feel: Optional[LookAndFeel] = None,
                 font_options: Optional[FontOptions] = None,
                 window_manager: Optional[WindowManager] = None,
                 error_handler: Optional[ErrorHandler] = None) -> None:
        super().__init__(toolkit, look_and_feel, font_options, window_manager,
                         error_handler)

        resolution = Dimension(bge.render.getWindowWidth(),
                               bge.render.getWindowHeight())

        self._resolution = BehaviorSubject(resolution)

        # noinspection PyTypeChecker
        self.window_size = self._resolution.pipe(ops.distinct_until_changed())

        self._shader = cast(GPUShader, gpu.shader.from_builtin("2D_IMAGE"))

        if use_viewport_render:
            # noinspection PyArgumentList
            self._draw_handler = SpaceView3D.draw_handler_add(
                self.process, (), "WINDOW", "POST_PIXEL")
        else:
            self._draw_handler = None

            bge.logic.getCurrentScene().post_draw.append(self.process)

        # noinspection PyTypeChecker
        self._texture = Buffer(bgl.GL_INT, 1)

        bgl.glGenTextures(1, self.texture)

    @property
    def shader(self) -> GPUShader:
        return self._shader

    @property
    def texture(self) -> Buffer:
        return self._texture

    def create_batch(self, size: Dimension) -> GPUBatch:
        if size is None:
            raise ValueError("Argument 'size' is required.")

        points = Bounds(0, 0, size.width, size.height).points

        vertices = tuple(map(lambda p: p.tuple, map(self.translate, points)))
        coords = ((0, 0), (1, 0), (1, 1), (0, 1))

        indices = {"pos": vertices, "texCoord": coords}

        return batch_for_shader(self.shader, "TRI_FAN", indices)

    def translate(self, point: Point) -> Point:
        if point is None:
            raise ValueError("Argument 'point' is required.")

        return point.copy(y=self.window_size.height - point.y)

    # noinspection PyTypeChecker
    def process_draw(self) -> None:
        width = bge.render.getWindowWidth()
        height = bge.render.getWindowHeight()

        self._resolution.on_next(Dimension(width, height))

        super().process_draw()

        data = self.surface.get_data()

        source = bgl.Buffer(bgl.GL_BYTE, width * height * 4, data)

        bgl.glEnable(bgl.GL_BLEND)
        bgl.glActiveTexture(bgl.GL_TEXTURE0)

        # noinspection PyUnresolvedReferences
        bgl.glBindTexture(bgl.GL_TEXTURE_2D, self.texture[0])

        bgl.glTexImage2D(bgl.GL_TEXTURE_2D, 0, bgl.GL_SRGB_ALPHA, width,
                         height, 0, bgl.GL_BGRA, bgl.GL_UNSIGNED_BYTE, source)

        bgl.glTexParameteri(bgl.GL_TEXTURE_2D, bgl.GL_TEXTURE_MIN_FILTER,
                            bgl.GL_NEAREST)
        bgl.glTexParameteri(bgl.GL_TEXTURE_2D, bgl.GL_TEXTURE_MAG_FILTER,
                            bgl.GL_NEAREST)

        self.shader.bind()
        self.shader.uniform_int("image", 0)

        self.batch.draw(self.shader)

        bgl.glDeleteBuffers(1, source)

    def dispose(self) -> None:
        if self._draw_handler:
            # noinspection PyArgumentList
            SpaceView3D.draw_handler_remove(self._draw_handler, "WINDOW")
        else:
            bge.logic.getCurrentScene().post_draw.remove(self.process)

        bgl.glDeleteTextures(1, self.texture)

        # noinspection PyTypeChecker
        bgl.glDeleteBuffers(1, self.texture)

        super().dispose()
Ejemplo n.º 8
0
class BlenderMouseInput(MouseInput, ReactiveObject, EventLoopAware):
    position: RV[Point] = rv.new_view()

    buttons: RV[int] = rv.new_view()

    def __init__(self, context: BlenderContext) -> None:
        super().__init__(context)

        self._position = Subject()
        self._activeInputs = Subject()

        # noinspection PyTypeChecker
        self.position = self._position.pipe(
            ops.distinct_until_changed(),
            ops.map(lambda v: tuple(
                p * s for p, s in zip(v, context.window_size.tuple))),
            ops.map(Point.from_tuple), ops.share())

        codes = {
            MouseButton.LEFT: bge.events.LEFTMOUSE,
            MouseButton.MIDDLE: bge.events.MIDDLEMOUSE,
            MouseButton.RIGHT: bge.events.RIGHTMOUSE
        }

        def pressed(e: SCA_InputEvent) -> bool:
            return KX_INPUT_ACTIVE in e.status or KX_INPUT_JUST_ACTIVATED in e.status

        def value_for(button: MouseButton) -> Observable:
            code = codes[button]

            return self._activeInputs.pipe(
                ops.start_with({}),
                ops.map(lambda i: code in i and pressed(i[code])),
                ops.map(lambda v: button if v else 0))

        # noinspection PyTypeChecker
        self.buttons = rx.combine_latest(
            *[value_for(b) for b in MouseButton]).pipe(
                ops.map(lambda v: reduce(lambda a, b: a | b, v)),
                ops.distinct_until_changed(), ops.share())

    @property
    def on_mouse_wheel(self) -> Observable:
        def on_wheel(code: int) -> Observable:
            return self._activeInputs.pipe(
                ops.filter(lambda i: code in i),
                ops.map(lambda i: i[code].values[-1]),
                ops.filter(lambda v: v != 0))

        return rx.merge(on_wheel(bge.events.WHEELUPMOUSE),
                        on_wheel(bge.events.WHEELDOWNMOUSE))

    def process(self) -> None:
        self._position.on_next(mouse.position)
        self._activeInputs.on_next(mouse.activeInputs)

    def dispose(self) -> None:
        super().dispose()

        self.execute_safely(self._position.dispose)
        self.execute_safely(self._activeInputs.dispose)