Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,3 +143,17 @@ It should work now.
## Flutter client

TBD

## Creating bridged controls

Bridged controls are the base controls which communicate directly with a corresponding Flutter widget in the client. A control can be created in several steps:
- Prequisite: setup a Flutter development environment
+ See [Flutter site](https://docs.flutter.dev/get-started/install) for instructions
- Add the control type to [flet/package/lib/src/models/control_type.dart](https://github.com/skeledrew/flet/blob/demo/EchoText/package/lib/src/models/control_type.dart)
- Implement the Flutter side control in [flet/package/lib/src/controls](https://github.com/skeledrew/flet/tree/demo/EchoText/package/lib/src/controls)
+ A demo is available as [echo_text.dart](https://github.com/skeledrew/flet/tree/demo/EchoText/package/lib/src/controls/echo_text.dart)
- Add the control to the type switch in [flet/package/lib/src/controls/create_control.dart](https://github.com/skeledrew/flet/blob/demo/EchoText/package/lib/src/controls/create_control.dart)
- Implement the Python side control in [flet/sdk/python/flet](https://github.com/skeledrew/flet/tree/demo/EchoText/sdk/python/flet)
+ A demo is available as [echo_text.py](https://github.com/skeledrew/flet/tree/demo/EchoText/sdk/python/flet/echo_text.py)
- Write a test for the control (optional but recommended)
+ See example and a helper Pytest fixture at [test_echo_text.py](https://github.com/skeledrew/flet/tree/demo/EchoText/sdk/python/tests/test_echo_text.py)
4 changes: 4 additions & 0 deletions package/lib/src/controls/create_control.dart
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import 'divider.dart';
import 'drag_target.dart';
import 'draggable.dart';
import 'dropdown.dart';
import 'echo_text.dart';
import 'elevated_button.dart';
import 'file_picker.dart';
import 'floating_action_button.dart';
Expand Down Expand Up @@ -78,6 +79,9 @@ Widget createControl(Control? parent, String id, bool parentDisabled) {
control: controlView.control,
children: controlView.children,
dispatch: controlView.dispatch);
case ControlType.echoText:
return EchoTextControl(
parent: parent, control: controlView.control);
case ControlType.text:
return TextControl(parent: parent, control: controlView.control);
case ControlType.icon:
Expand Down
80 changes: 80 additions & 0 deletions package/lib/src/controls/echo_text.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import 'package:flutter/material.dart';
import 'package:flutter_redux/flutter_redux.dart';

import '../flet_app_services.dart';
import '../models/app_state.dart';
import '../models/control.dart';


class EchoTextControl extends StatefulWidget {
final Control? parent;
final Control control;

const EchoTextControl(
{Key? key,
this.parent,
required this.control}
) : super(key: key);

@override
State<EchoTextControl> createState() => _EchoTextControlState();
}

class _EchoTextControlState extends State<EchoTextControl> {
String _message = "";
String _echoed = "N/A";

@override
Widget build(BuildContext context) {
final ws = FletAppServices.of(context).ws; // websocket

Function() onPress = () {
ws.pageEventFromWeb(
eventTarget: widget.control.id,
eventName: "click",
eventData: "",
);
};

return StoreConnector<AppState, Function>(
distinct: true,
converter: (store) => store.dispatch,
builder: (context, dispatch) {
String message = widget.control.attrs["message"] ?? "";
if (_message != message) {
_message = message;
}
String echoed = widget.control.attrs["echoed"] ?? "";
if (_echoed != echoed) {
_echoed = echoed;
}

return Column(
children: [
TextFormField(
onChanged: (String value) {
setState(() {
_message = value;
});
List<Map<String, String>> props = [
{"i": widget.control.id, "message": value}
];
ws.updateControlProps(props: props);
ws.pageEventFromWeb(
eventTarget: widget.control.id,
eventName: "change",
eventData: value,
);
},
initialValue: _message,
),
TextButton(
onPressed: onPress,
child: const Text("send")
),
Text(echoed)
]
);
});
}
}
1 change: 1 addition & 0 deletions package/lib/src/models/control_type.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ enum ControlType {
dragTarget,
dropdown,
dropdownOption,
echoText,
elevatedButton,
filePicker,
floatingActionButton,
Expand Down
50 changes: 50 additions & 0 deletions sdk/python/flet/echo_text.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
"""Echo text control demo."""


import flet
from flet.constrained_control import ConstrainedControl


class EchoText(ConstrainedControl):
def __init__(self, message: str = None, **kwds):
super().__init__(**kwds)
self._add_event_handler("click", self.on_click)
self.message = message
return

def _get_control_name(self):
return "echotext"

def _get_children(self):
return []

@property
def echoed(self):
"""Access the Text widget containing echoed text."""
return self._get_attr("echoed")

@echoed.setter
def echoed(self, value: str):
return self._set_attr("echoed", value)

@property
def message(self):
"""Access the TextField widget where text is entered."""
return self._get_attr("message")

@message.setter
def message(self, value: str):
return self._set_attr("message", value)

# def on_change(self, e: flet.Event):
# self.message = e.data
# return

def on_click(self, e: flet.Event):
"""Handle button click that updates the `echoed`.

This gets whatever is currently typed, changes and sends it back.
"""
self.echoed = f'I got "{self.message}"...'
self.update()
return
137 changes: 137 additions & 0 deletions sdk/python/tests/test_echo_text.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
"""Test echo text."""

import os
from pathlib import Path

import pyautogui as pag
import pytest
from beartype import beartype

import flet
from flet.protocol import Command
from flet.echo_text import EchoText


def test_echo_text_add():
et = EchoText(message="Hello world")
assert et._build_add_commands() == [
Command(0, None, values=["echotext"], attrs={"message": "Hello world"})
]
return


def test_echo_text_INTE(run_target):
title = "EchoText test"

@beartype
def main_func(page: flet.Page):
page.title = title
et = EchoText(message="Hello world")
page.add(et)
return
page: flet.Page = run_target(target=main_func)
# NOTE: may need a wait here to ensure window is active before continuing
pag.press("tab")
pag.hotkey("ctrl", "a")
pag.write("hi ")
et = page.controls[0]
assert et.message == "hi "
assert et.echoed is None
pag.write("there")
pag.press(["tab", "space"])
assert et.echoed == 'I got "hi there"...'
return


@pytest.fixture
def run_target():
holder = {}

def _run(cleanup=False, **kwds):
app(holder=holder, **kwds)
page = holder["page"]

if cleanup:
page.controls.clear()
page.update()
return page
yield _run
holder["conn"].close()
holder["fvp"].kill()
return


def app(
holder=None,
name="",
host=None,
port=0,
target=None,
permissions=None,
view: flet.AppViewer = flet.FLET_APP,
assets_dir=None,
upload_dir=None,
web_renderer="canvaskit",
route_url_strategy="hash",
):

if target is None:
raise Exception("target argument is not specified")

conn = flet.flet._connect_internal(
page_name=name,
host=host,
port=port,
is_app=True,
permissions=permissions,
session_handler=target,
assets_dir=assets_dir,
upload_dir=upload_dir,
web_renderer=web_renderer,
route_url_strategy=route_url_strategy,
)

url_prefix = os.getenv("FLET_DISPLAY_URL_PREFIX")
if url_prefix is not None:
print(url_prefix, conn.page_url)
else:
print(f"App URL: {conn.page_url}")

import time
fvp = open_flet_view(
conn.page_url,
False,
f"/tmp/local-flet-{flet.version.version}"
)
print("Waiting for session to be created...")

while not conn.sessions:
time.sleep(0.3)
page = list(conn.sessions.values())[0]

if holder is not None:
holder.update({
"conn": conn,
"fvp": fvp,
"page": page,
})
return conn, page, fvp


def open_flet_view(page_url, hidden, viewer_path=None):
import subprocess as sp

args = []

if viewer_path and Path(viewer_path).exists():
viewer_path = Path(viewer_path)

else:
raise OSError(f'unable to find viewer at "{viewer_path}"; try compiling and/or copying it there')
app_path = viewer_path / "flet"
args = [str(app_path), page_url]
flet_env = {**os.environ}

if hidden:
flet_env["FLET_HIDE_WINDOW_ON_START"] = "true"
return sp.Popen(args, env=flet_env)