chrome: greatly improve soft-keyboard visibility & layout handling
Some checks failed
CI / Rustfmt + Clippy (push) Has been cancelled
CI / Check (android) (push) Has been cancelled
CI / Test (Linux) (push) Has been cancelled
CI / Test (macOS) (push) Has been cancelled
CI / Test (Windows) (push) Has been cancelled
CI / rpm/deb (aarch64) (push) Has been cancelled
CI / rpm/deb (x86_64) (push) Has been cancelled
CI / macOS dmg (aarch64) (push) Has been cancelled
CI / macOS dmg (x86_64) (push) Has been cancelled
CI / Windows Installer (aarch64) (push) Has been cancelled
CI / Windows Installer (x86_64) (push) Has been cancelled
CI / Upload Artifacts to Server (push) Has been cancelled
Some checks failed
CI / Rustfmt + Clippy (push) Has been cancelled
CI / Check (android) (push) Has been cancelled
CI / Test (Linux) (push) Has been cancelled
CI / Test (macOS) (push) Has been cancelled
CI / Test (Windows) (push) Has been cancelled
CI / rpm/deb (aarch64) (push) Has been cancelled
CI / rpm/deb (x86_64) (push) Has been cancelled
CI / macOS dmg (aarch64) (push) Has been cancelled
CI / macOS dmg (x86_64) (push) Has been cancelled
CI / Windows Installer (aarch64) (push) Has been cancelled
CI / Windows Installer (x86_64) (push) Has been cancelled
CI / Upload Artifacts to Server (push) Has been cancelled
This reworks how we detect and respond to the on-screen keyboard so inputs don’t get buried and the UI doesn’t “jump”. - Add SoftKeyboardAnim + AnimState FSM for smooth IME open/close animation - Centralize logic in keyboard_visibility() with clear edge states - Animate keyboard height via animate_value_with_time instead of layer transforms - Add ChromeOptions::KeyboardVisibility flag when focused input would be occluded - Add SidebarOptions::Compact to collapse sidebar while typing - Hide mobile toolbar when keyboard is open (columns app) - Use .stick_to_bottom(true) in reply + profile editors; remove old spacer hack - Virtual keyboard toggle moved to F1 in Debug builds - Introduce SoftKeyboardContext::platform(ctx) helper - Cleanup dead/commented code and wire up soft_kb_anim_state in Chrome Result: inputs stay visible, open/close is smooth, and UI adjusts gracefully when typing. Signed-off-by: William Casarin <jb55@jb55.com>
This commit is contained in:
@@ -40,6 +40,14 @@ pub enum SoftKeyboardContext {
|
|||||||
Platform { ppp: f32 },
|
Platform { ppp: f32 },
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl SoftKeyboardContext {
|
||||||
|
pub fn platform(context: &egui::Context) -> Self {
|
||||||
|
Self::Platform {
|
||||||
|
ppp: context.pixels_per_point(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl<'a> AppContext<'a> {
|
impl<'a> AppContext<'a> {
|
||||||
pub fn soft_keyboard_rect(&self, screen_rect: Rect, ctx: SoftKeyboardContext) -> Option<Rect> {
|
pub fn soft_keyboard_rect(&self, screen_rect: Rect, ctx: SoftKeyboardContext) -> Option<Rect> {
|
||||||
match ctx {
|
match ctx {
|
||||||
@@ -53,8 +61,13 @@ impl<'a> AppContext<'a> {
|
|||||||
#[cfg(target_os = "android")]
|
#[cfg(target_os = "android")]
|
||||||
{
|
{
|
||||||
use android_activity::InsetType;
|
use android_activity::InsetType;
|
||||||
|
|
||||||
|
// not sure why I need this, it seems to be consistently off by some amount of
|
||||||
|
// pixels ?
|
||||||
|
let fudge = 0.0;
|
||||||
|
|
||||||
let inset = self.android.get_window_insets(InsetType::Ime);
|
let inset = self.android.get_window_insets(InsetType::Ime);
|
||||||
let height = inset.bottom as f32 / ppp;
|
let height = (inset.bottom as f32 / ppp) - fudge;
|
||||||
skb_rect_from_screen_rect(screen_rect, height)
|
skb_rect_from_screen_rect(screen_rect, height)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
//use wasm_bindgen::prelude::*;
|
//use wasm_bindgen::prelude::*;
|
||||||
use crate::app::NotedeckApp;
|
use crate::app::NotedeckApp;
|
||||||
use crate::ChromeOptions;
|
use crate::ChromeOptions;
|
||||||
|
use bitflags::bitflags;
|
||||||
use eframe::CreationContext;
|
use eframe::CreationContext;
|
||||||
use egui::{vec2, Button, Color32, Label, Layout, Rect, RichText, ThemePreference, Widget};
|
use egui::{vec2, Button, Color32, Label, Layout, Rect, RichText, ThemePreference, Widget};
|
||||||
use egui_extras::{Size, StripBuilder};
|
use egui_extras::{Size, StripBuilder};
|
||||||
@@ -25,6 +26,10 @@ pub struct Chrome {
|
|||||||
active: i32,
|
active: i32,
|
||||||
options: ChromeOptions,
|
options: ChromeOptions,
|
||||||
apps: Vec<NotedeckApp>,
|
apps: Vec<NotedeckApp>,
|
||||||
|
|
||||||
|
/// The state of the soft keyboard animation
|
||||||
|
soft_kb_anim_state: AnimState,
|
||||||
|
|
||||||
pub repaint_causes: HashMap<egui::RepaintCause, u64>,
|
pub repaint_causes: HashMap<egui::RepaintCause, u64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,6 +42,14 @@ pub enum ChromePanelAction {
|
|||||||
Profile(notedeck::enostr::Pubkey),
|
Profile(notedeck::enostr::Pubkey),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bitflags! {
|
||||||
|
#[repr(transparent)]
|
||||||
|
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
|
pub struct SidebarOptions: u8 {
|
||||||
|
const Compact = 1 << 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl ChromePanelAction {
|
impl ChromePanelAction {
|
||||||
fn columns_navigate(ctx: &mut AppContext, chrome: &mut Chrome, route: notedeck_columns::Route) {
|
fn columns_navigate(ctx: &mut AppContext, chrome: &mut Chrome, route: notedeck_columns::Route) {
|
||||||
chrome.switch_to_columns();
|
chrome.switch_to_columns();
|
||||||
@@ -174,6 +187,7 @@ impl Chrome {
|
|||||||
app_ctx: &mut AppContext,
|
app_ctx: &mut AppContext,
|
||||||
builder: StripBuilder,
|
builder: StripBuilder,
|
||||||
amt_open: f32,
|
amt_open: f32,
|
||||||
|
amt_keyboard_open: f32,
|
||||||
) -> Option<ChromePanelAction> {
|
) -> Option<ChromePanelAction> {
|
||||||
let mut got_action: Option<ChromePanelAction> = None;
|
let mut got_action: Option<ChromePanelAction> = None;
|
||||||
|
|
||||||
@@ -204,9 +218,17 @@ impl Chrome {
|
|||||||
self.topdown_sidebar(ui, app_ctx.i18n);
|
self.topdown_sidebar(ui, app_ctx.i18n);
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
vstrip.cell(|ui| {
|
vstrip.cell(|ui| {
|
||||||
ui.with_layout(Layout::bottom_up(egui::Align::Center), |ui| {
|
ui.with_layout(Layout::bottom_up(egui::Align::Center), |ui| {
|
||||||
if let Some(action) = bottomup_sidebar(self, app_ctx, ui) {
|
let options = if amt_keyboard_open > 0.0 {
|
||||||
|
SidebarOptions::Compact
|
||||||
|
} else {
|
||||||
|
SidebarOptions::default()
|
||||||
|
};
|
||||||
|
if let Some(action) =
|
||||||
|
bottomup_sidebar(self, app_ctx, ui, options)
|
||||||
|
{
|
||||||
got_action = Some(action);
|
got_action = Some(action);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -250,7 +272,6 @@ impl Chrome {
|
|||||||
.animate_bool(open_id, self.options.contains(ChromeOptions::IsOpen))
|
.animate_bool(open_id, self.options.contains(ChromeOptions::IsOpen))
|
||||||
* side_panel_width
|
* side_panel_width
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Show the side menu or bar, depending on if we're on a narrow
|
/// Show the side menu or bar, depending on if we're on a narrow
|
||||||
/// or wide screen.
|
/// or wide screen.
|
||||||
///
|
///
|
||||||
@@ -259,52 +280,54 @@ impl Chrome {
|
|||||||
fn show(&mut self, ctx: &mut AppContext, ui: &mut egui::Ui) -> Option<ChromePanelAction> {
|
fn show(&mut self, ctx: &mut AppContext, ui: &mut egui::Ui) -> Option<ChromePanelAction> {
|
||||||
ui.spacing_mut().item_spacing.x = 0.0;
|
ui.spacing_mut().item_spacing.x = 0.0;
|
||||||
|
|
||||||
if ctx.args.options.contains(NotedeckOptions::Debug)
|
|
||||||
&& ui.ctx().input(|i| i.key_pressed(egui::Key::Backtick))
|
|
||||||
{
|
|
||||||
self.options.toggle(ChromeOptions::VirtualKeyboard);
|
|
||||||
}
|
|
||||||
|
|
||||||
let amt_open = self.amount_open(ui);
|
let amt_open = self.amount_open(ui);
|
||||||
let r = self.panel(ctx, StripBuilder::new(ui), amt_open);
|
let skb_anim =
|
||||||
|
keyboard_visibility(ui, ctx, &mut self.options, &mut self.soft_kb_anim_state);
|
||||||
|
|
||||||
let skb_ctx = if self.options.contains(ChromeOptions::VirtualKeyboard) {
|
let virtual_keyboard = self.options.contains(ChromeOptions::VirtualKeyboard);
|
||||||
SoftKeyboardContext::Virtual
|
let keyboard_height = if self.options.contains(ChromeOptions::KeyboardVisibility) {
|
||||||
|
skb_anim.anim_height
|
||||||
} else {
|
} else {
|
||||||
SoftKeyboardContext::Platform {
|
0.0
|
||||||
ppp: ui.ctx().pixels_per_point(),
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// move screen up if virtual keyboard intersects with input_rect
|
// if the soft keyboard is open, shrink the chrome contents
|
||||||
let screen_rect = ui.ctx().screen_rect();
|
let mut action: Option<ChromePanelAction> = None;
|
||||||
let mut keyboard_height = 0.0;
|
// build a strip to carve out the soft keyboard inset
|
||||||
if let Some(vkb_rect) = ctx.soft_keyboard_rect(screen_rect, skb_ctx.clone()) {
|
StripBuilder::new(ui)
|
||||||
if let SoftKeyboardContext::Virtual = skb_ctx {
|
.size(Size::remainder())
|
||||||
virtual_keyboard_ui(ui, vkb_rect);
|
.size(Size::exact(keyboard_height))
|
||||||
|
.vertical(|mut strip| {
|
||||||
|
// the actual content, shifted up because of the soft keyboard
|
||||||
|
strip.cell(|ui| {
|
||||||
|
action = self.panel(ctx, StripBuilder::new(ui), amt_open, keyboard_height);
|
||||||
|
});
|
||||||
|
|
||||||
|
// the filler space taken up by the soft keyboard
|
||||||
|
strip.cell(|ui| {
|
||||||
|
// keyboard-visibility virtual keyboard
|
||||||
|
if virtual_keyboard && keyboard_height > 0.0 {
|
||||||
|
tracing::debug!("got here");
|
||||||
|
virtual_keyboard_ui(ui, ui.available_rect_before_wrap())
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// hovering virtual keyboard
|
||||||
|
if virtual_keyboard {
|
||||||
|
if let Some(mut kb_rect) = skb_anim.skb_rect {
|
||||||
|
let kb_height = if self.options.contains(ChromeOptions::KeyboardVisibility) {
|
||||||
|
keyboard_height
|
||||||
|
} else {
|
||||||
|
400.0
|
||||||
|
};
|
||||||
|
kb_rect.min.y = kb_rect.max.y - kb_height;
|
||||||
|
tracing::debug!("hovering virtual kb_height:{keyboard_height} kb_rect:{kb_rect}");
|
||||||
|
virtual_keyboard_ui(ui, kb_rect)
|
||||||
}
|
}
|
||||||
if let Some(input_rect) = notedeck_ui::input_rect(ui) {
|
|
||||||
if input_rect.intersects(vkb_rect) {
|
|
||||||
tracing::debug!("screen:{screen_rect} skb:{vkb_rect}");
|
|
||||||
keyboard_height = vkb_rect.height();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// clear last input box position state
|
|
||||||
notedeck_ui::clear_input_rect(ui);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let anim_height =
|
action
|
||||||
ui.ctx()
|
|
||||||
.animate_value_with_time(egui::Id::new("keyboard_anim"), keyboard_height, 0.1);
|
|
||||||
if anim_height > 0.0 {
|
|
||||||
ui.ctx().transform_layer_shapes(
|
|
||||||
ui.layer_id(),
|
|
||||||
egui::emath::TSTransform::from_translation(egui::Vec2::new(0.0, -anim_height)),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
r
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn topdown_sidebar(&mut self, ui: &mut egui::Ui, i18n: &mut Localization) {
|
fn topdown_sidebar(&mut self, ui: &mut egui::Ui, i18n: &mut Localization) {
|
||||||
@@ -652,38 +675,6 @@ fn pfp_button(ctx: &mut AppContext, ui: &mut egui::Ui) -> egui::Response {
|
|||||||
ui.put(helper.get_animation_rect(), &mut widget);
|
ui.put(helper.get_animation_rect(), &mut widget);
|
||||||
|
|
||||||
helper.take_animation_response()
|
helper.take_animation_response()
|
||||||
|
|
||||||
// let selected = ctx.accounts.cache.selected();
|
|
||||||
|
|
||||||
// pfp_resp.context_menu(|ui| {
|
|
||||||
// for (pk, account) in &ctx.accounts.cache {
|
|
||||||
// let profile = ctx.ndb.get_profile_by_pubkey(&txn, pk).ok();
|
|
||||||
// let is_selected = *pk == selected.key.pubkey;
|
|
||||||
// let has_nsec = account.key.secret_key.is_some();
|
|
||||||
|
|
||||||
// let profile_peview_view = {
|
|
||||||
// let max_size = egui::vec2(ui.available_width(), 77.0);
|
|
||||||
// let resp = ui.allocate_response(max_size, egui::Sense::click());
|
|
||||||
// ui.allocate_new_ui(UiBuilder::new().max_rect(resp.rect), |ui| {
|
|
||||||
// ui.add(
|
|
||||||
// &mut ProfilePic::new(ctx.img_cache, get_profile_url(profile.as_ref()))
|
|
||||||
// .size(24.0),
|
|
||||||
// )
|
|
||||||
// })
|
|
||||||
// };
|
|
||||||
|
|
||||||
// // if let Some(op) = profile_peview_view {
|
|
||||||
// // return_op = Some(match op {
|
|
||||||
// // ProfilePreviewAction::SwitchTo => AccountsViewResponse::SelectAccount(*pk),
|
|
||||||
// // ProfilePreviewAction::RemoveAccount => AccountsViewResponse::RemoveAccount(*pk),
|
|
||||||
// // });
|
|
||||||
// // }
|
|
||||||
// }
|
|
||||||
// // if ui.menu_image_button(image, add_contents).clicked() {
|
|
||||||
// // // ui.ctx().copy_text(url.to_owned());
|
|
||||||
// // ui.close_menu();
|
|
||||||
// // }
|
|
||||||
// });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The section of the chrome sidebar that starts at the
|
/// The section of the chrome sidebar that starts at the
|
||||||
@@ -692,10 +683,23 @@ fn bottomup_sidebar(
|
|||||||
chrome: &mut Chrome,
|
chrome: &mut Chrome,
|
||||||
ctx: &mut AppContext,
|
ctx: &mut AppContext,
|
||||||
ui: &mut egui::Ui,
|
ui: &mut egui::Ui,
|
||||||
|
options: SidebarOptions,
|
||||||
) -> Option<ChromePanelAction> {
|
) -> Option<ChromePanelAction> {
|
||||||
ui.add_space(8.0);
|
ui.add_space(8.0);
|
||||||
|
|
||||||
let pfp_resp = pfp_button(ctx, ui).on_hover_cursor(egui::CursorIcon::PointingHand);
|
let pfp_resp = pfp_button(ctx, ui).on_hover_cursor(egui::CursorIcon::PointingHand);
|
||||||
|
|
||||||
|
// we skip this whole function in compact mode
|
||||||
|
if options.contains(SidebarOptions::Compact) {
|
||||||
|
return if pfp_resp.clicked() {
|
||||||
|
Some(ChromePanelAction::Profile(
|
||||||
|
ctx.accounts.get_selected_account().key.pubkey,
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
let accounts_resp = accounts_button(ui).on_hover_cursor(egui::CursorIcon::PointingHand);
|
let accounts_resp = accounts_button(ui).on_hover_cursor(egui::CursorIcon::PointingHand);
|
||||||
let settings_resp = settings_button(ui).on_hover_cursor(egui::CursorIcon::PointingHand);
|
let settings_resp = settings_button(ui).on_hover_cursor(egui::CursorIcon::PointingHand);
|
||||||
|
|
||||||
@@ -956,3 +960,218 @@ fn virtual_keyboard_ui(ui: &mut egui::Ui, rect: egui::Rect) {
|
|||||||
.response
|
.response
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct SoftKeyboardAnim {
|
||||||
|
skb_rect: Option<Rect>,
|
||||||
|
anim_height: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Default, Clone, Eq, PartialEq, Debug)]
|
||||||
|
enum AnimState {
|
||||||
|
/// It finished opening
|
||||||
|
Opened,
|
||||||
|
|
||||||
|
/// We started to open
|
||||||
|
StartOpen,
|
||||||
|
|
||||||
|
/// We started to close
|
||||||
|
StartClose,
|
||||||
|
|
||||||
|
/// We finished openning
|
||||||
|
FinishedOpen,
|
||||||
|
|
||||||
|
/// We finished to close
|
||||||
|
FinishedClose,
|
||||||
|
|
||||||
|
/// It finished closing
|
||||||
|
#[default]
|
||||||
|
Closed,
|
||||||
|
|
||||||
|
/// We are animating towards open
|
||||||
|
Opening,
|
||||||
|
|
||||||
|
/// We are animating towards close
|
||||||
|
Closing,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SoftKeyboardAnim {
|
||||||
|
/// Advance the FSM based on current (anim_height) vs target (skb_rect.height()).
|
||||||
|
/// Start*/Finished* are one-tick edge states used for signaling.
|
||||||
|
fn changed(&self, state: AnimState) -> AnimState {
|
||||||
|
const EPS: f32 = 0.01;
|
||||||
|
|
||||||
|
let target = self.skb_rect.map_or(0.0, |r| r.height());
|
||||||
|
let current = self.anim_height;
|
||||||
|
|
||||||
|
let done = (current - target).abs() <= EPS;
|
||||||
|
let going_up = target > current + EPS;
|
||||||
|
let going_down = current > target + EPS;
|
||||||
|
let target_is_closed = target <= EPS;
|
||||||
|
|
||||||
|
match state {
|
||||||
|
// Resting states: emit a Start* edge only when a move is requested,
|
||||||
|
// and pick direction by the sign of (target - current).
|
||||||
|
AnimState::Opened => {
|
||||||
|
if done {
|
||||||
|
AnimState::Opened
|
||||||
|
} else if going_up {
|
||||||
|
AnimState::StartOpen
|
||||||
|
} else {
|
||||||
|
AnimState::StartClose
|
||||||
|
}
|
||||||
|
}
|
||||||
|
AnimState::Closed => {
|
||||||
|
if done {
|
||||||
|
AnimState::Closed
|
||||||
|
} else if going_up {
|
||||||
|
AnimState::StartOpen
|
||||||
|
} else {
|
||||||
|
AnimState::StartClose
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Edge → flow
|
||||||
|
AnimState::StartOpen => AnimState::Opening,
|
||||||
|
AnimState::StartClose => AnimState::Closing,
|
||||||
|
|
||||||
|
// Flow states: finish when we hit the target; if the target jumps across,
|
||||||
|
// emit the opposite Start* to signal a reversal.
|
||||||
|
AnimState::Opening => {
|
||||||
|
if done {
|
||||||
|
if target_is_closed {
|
||||||
|
AnimState::FinishedClose
|
||||||
|
} else {
|
||||||
|
AnimState::FinishedOpen
|
||||||
|
}
|
||||||
|
} else if going_down {
|
||||||
|
// target moved below current mid-flight → reversal
|
||||||
|
AnimState::StartClose
|
||||||
|
} else {
|
||||||
|
AnimState::Opening
|
||||||
|
}
|
||||||
|
}
|
||||||
|
AnimState::Closing => {
|
||||||
|
if done {
|
||||||
|
if target_is_closed {
|
||||||
|
AnimState::FinishedClose
|
||||||
|
} else {
|
||||||
|
AnimState::FinishedOpen
|
||||||
|
}
|
||||||
|
} else if going_up {
|
||||||
|
// target moved above current mid-flight → reversal
|
||||||
|
AnimState::StartOpen
|
||||||
|
} else {
|
||||||
|
AnimState::Closing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finish edges collapse to the stable resting states on the next tick.
|
||||||
|
AnimState::FinishedOpen => AnimState::Opened,
|
||||||
|
AnimState::FinishedClose => AnimState::Closed,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// How "open" the softkeyboard is. This is an animated value
|
||||||
|
fn soft_keyboard_anim(
|
||||||
|
ui: &mut egui::Ui,
|
||||||
|
ctx: &mut AppContext,
|
||||||
|
chrome_options: &mut ChromeOptions,
|
||||||
|
) -> SoftKeyboardAnim {
|
||||||
|
let skb_ctx = if chrome_options.contains(ChromeOptions::VirtualKeyboard) {
|
||||||
|
SoftKeyboardContext::Virtual
|
||||||
|
} else {
|
||||||
|
SoftKeyboardContext::Platform {
|
||||||
|
ppp: ui.ctx().pixels_per_point(),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// move screen up if virtual keyboard intersects with input_rect
|
||||||
|
let screen_rect = ui.ctx().screen_rect();
|
||||||
|
let mut skb_rect: Option<Rect> = None;
|
||||||
|
|
||||||
|
let keyboard_height =
|
||||||
|
if let Some(vkb_rect) = ctx.soft_keyboard_rect(screen_rect, skb_ctx.clone()) {
|
||||||
|
skb_rect = Some(vkb_rect);
|
||||||
|
vkb_rect.height()
|
||||||
|
} else {
|
||||||
|
0.0
|
||||||
|
};
|
||||||
|
|
||||||
|
let anim_height =
|
||||||
|
ui.ctx()
|
||||||
|
.animate_value_with_time(egui::Id::new("keyboard_anim"), keyboard_height, 0.1);
|
||||||
|
|
||||||
|
SoftKeyboardAnim {
|
||||||
|
anim_height,
|
||||||
|
skb_rect,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn try_toggle_virtual_keyboard(
|
||||||
|
ctx: &egui::Context,
|
||||||
|
options: NotedeckOptions,
|
||||||
|
chrome_options: &mut ChromeOptions,
|
||||||
|
) {
|
||||||
|
// handle virtual keyboard toggle here because why not
|
||||||
|
if options.contains(NotedeckOptions::Debug) && ctx.input(|i| i.key_pressed(egui::Key::F1)) {
|
||||||
|
chrome_options.toggle(ChromeOptions::VirtualKeyboard);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// All the logic which handles our keyboard visibility
|
||||||
|
fn keyboard_visibility(
|
||||||
|
ui: &mut egui::Ui,
|
||||||
|
ctx: &mut AppContext,
|
||||||
|
options: &mut ChromeOptions,
|
||||||
|
soft_kb_anim_state: &mut AnimState,
|
||||||
|
) -> SoftKeyboardAnim {
|
||||||
|
try_toggle_virtual_keyboard(ui.ctx(), ctx.args.options, options);
|
||||||
|
|
||||||
|
let soft_kb_anim = soft_keyboard_anim(ui, ctx, options);
|
||||||
|
|
||||||
|
let prev_state = *soft_kb_anim_state;
|
||||||
|
let current_state = soft_kb_anim.changed(prev_state);
|
||||||
|
*soft_kb_anim_state = current_state;
|
||||||
|
|
||||||
|
if prev_state != current_state {
|
||||||
|
tracing::debug!("soft kb state {prev_state:?} -> {current_state:?}");
|
||||||
|
}
|
||||||
|
|
||||||
|
match current_state {
|
||||||
|
// we finished
|
||||||
|
AnimState::FinishedOpen => {}
|
||||||
|
|
||||||
|
// on first open, we setup our scroll target
|
||||||
|
AnimState::StartOpen => {
|
||||||
|
// when we first open the keyboard, check to see if the target soft
|
||||||
|
// keyboard rect (the height at full open) intersects with any
|
||||||
|
// input response rects from last frame
|
||||||
|
//
|
||||||
|
// If we do, then we set a bit that we need keyboard visibility.
|
||||||
|
// We will use this bit to resize the screen based on the soft
|
||||||
|
// keyboard animation state
|
||||||
|
if let Some(skb_rect) = soft_kb_anim.skb_rect {
|
||||||
|
if let Some(input_rect) = notedeck_ui::input_rect(ui) {
|
||||||
|
options.set(
|
||||||
|
ChromeOptions::KeyboardVisibility,
|
||||||
|
input_rect.intersects(skb_rect),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AnimState::FinishedClose => {
|
||||||
|
// clear last input box position state
|
||||||
|
notedeck_ui::clear_input_rect(ui);
|
||||||
|
}
|
||||||
|
|
||||||
|
AnimState::Closing => {}
|
||||||
|
AnimState::Opened => {}
|
||||||
|
AnimState::Closed => {}
|
||||||
|
AnimState::Opening => {}
|
||||||
|
AnimState::StartClose => {}
|
||||||
|
};
|
||||||
|
|
||||||
|
soft_kb_anim
|
||||||
|
}
|
||||||
|
|||||||
@@ -20,6 +20,9 @@ bitflags! {
|
|||||||
|
|
||||||
/// Repaint debug
|
/// Repaint debug
|
||||||
const RepaintDebug = 1 << 3;
|
const RepaintDebug = 1 << 3;
|
||||||
|
|
||||||
|
/// We need soft keyboard visibility
|
||||||
|
const KeyboardVisibility = 1 << 4;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -644,10 +644,20 @@ fn render_damus_mobile(
|
|||||||
|
|
||||||
let active_col = app.columns_mut(app_ctx.i18n, app_ctx.accounts).selected as usize;
|
let active_col = app.columns_mut(app_ctx.i18n, app_ctx.accounts).selected as usize;
|
||||||
let mut app_action: Option<AppAction> = None;
|
let mut app_action: Option<AppAction> = None;
|
||||||
|
// don't show toolbar if soft keyboard is open
|
||||||
|
let skb_rect = app_ctx.soft_keyboard_rect(
|
||||||
|
ui.ctx().screen_rect(),
|
||||||
|
notedeck::SoftKeyboardContext::platform(ui.ctx()),
|
||||||
|
);
|
||||||
|
let toolbar_height = if skb_rect.is_none() {
|
||||||
|
Damus::toolbar_height()
|
||||||
|
} else {
|
||||||
|
0.0
|
||||||
|
};
|
||||||
|
|
||||||
StripBuilder::new(ui)
|
StripBuilder::new(ui)
|
||||||
.size(Size::remainder()) // top cell
|
.size(Size::remainder()) // top cell
|
||||||
.size(Size::exact(Damus::toolbar_height())) // bottom cell
|
.size(Size::exact(toolbar_height)) // bottom cell
|
||||||
.vertical(|mut strip| {
|
.vertical(|mut strip| {
|
||||||
strip.cell(|ui| {
|
strip.cell(|ui| {
|
||||||
let rect = ui.available_rect_before_wrap();
|
let rect = ui.available_rect_before_wrap();
|
||||||
@@ -678,17 +688,22 @@ fn render_damus_mobile(
|
|||||||
hovering_post_button(ui, app, app_ctx, rect);
|
hovering_post_button(ui, app, app_ctx, rect);
|
||||||
});
|
});
|
||||||
|
|
||||||
strip.cell(|ui| {
|
strip.cell(|ui| 'brk: {
|
||||||
|
if toolbar_height <= 0.0 {
|
||||||
|
break 'brk;
|
||||||
|
}
|
||||||
|
|
||||||
let unseen_notif = unseen_notification(
|
let unseen_notif = unseen_notification(
|
||||||
app,
|
app,
|
||||||
app_ctx.ndb,
|
app_ctx.ndb,
|
||||||
app_ctx.accounts.get_selected_account().key.pubkey,
|
app_ctx.accounts.get_selected_account().key.pubkey,
|
||||||
);
|
);
|
||||||
|
|
||||||
let resp = toolbar(ui, unseen_notif);
|
if skb_rect.is_none() {
|
||||||
|
let resp = toolbar(ui, unseen_notif);
|
||||||
if let Some(action) = resp {
|
if let Some(action) = resp {
|
||||||
action.process(app, app_ctx);
|
action.process(app, app_ctx);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ impl<'a, 'd> PostReplyView<'a, 'd> {
|
|||||||
pub fn show(&mut self, ui: &mut egui::Ui) -> PostResponse {
|
pub fn show(&mut self, ui: &mut egui::Ui) -> PostResponse {
|
||||||
ScrollArea::vertical()
|
ScrollArea::vertical()
|
||||||
.id_salt(self.scroll_id)
|
.id_salt(self.scroll_id)
|
||||||
|
.stick_to_bottom(true)
|
||||||
.show(ui, |ui| self.show_internal(ui))
|
.show(ui, |ui| self.show_internal(ui))
|
||||||
.inner
|
.inner
|
||||||
}
|
}
|
||||||
@@ -121,7 +122,7 @@ impl<'a, 'd> PostReplyView<'a, 'd> {
|
|||||||
// large and things start breaking. I think this is an ok
|
// large and things start breaking. I think this is an ok
|
||||||
// solution but there could be a better one.
|
// solution but there could be a better one.
|
||||||
//
|
//
|
||||||
ui.add_space(500.0);
|
//ui.add_space(500.0);
|
||||||
|
|
||||||
post_response
|
post_response
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ impl<'a> EditProfileView<'a> {
|
|||||||
pub fn ui(&mut self, ui: &mut egui::Ui) -> bool {
|
pub fn ui(&mut self, ui: &mut egui::Ui) -> bool {
|
||||||
ScrollArea::vertical()
|
ScrollArea::vertical()
|
||||||
.id_salt(EditProfileView::scroll_id())
|
.id_salt(EditProfileView::scroll_id())
|
||||||
|
.stick_to_bottom(true)
|
||||||
.show(ui, |ui| {
|
.show(ui, |ui| {
|
||||||
banner(ui, self.state.banner(), 188.0);
|
banner(ui, self.state.banner(), 188.0);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user